printf
私のシステムでは、次の 2 つの方法で通話できます。
$ type -a printf
printf is a shell builtin
printf is /usr/bin/printf
$ file /usr/bin/printf
/usr/bin/printf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked (uses shared libs), for GNU/Linux 2.6.32,
BuildID[sha1]=d663d220e5c2a2fc57462668d84d2f72d0563c33, stripped
つまり、1 つは bash 組み込みで、もう 1 つは適切にコンパイルされた実行ファイルです。printf
シェル組み込みよりもはるかに高速になることだけを目的としたプログラムを期待していました。確かに組み込みは既にメモリにロードされていますが、実際の実行時間は専用プログラムの方が速いはずですよね? Unix 哲学の最高の部分で、1 つのことを非常にうまく実行するように最適化されます。
どうやらそうではないようです:
$ >/tmp/foo; time for i in `seq 1 3000`; do printf '%s ' "$i" >> /tmp/foo; done;
real 0m0.065s
user 0m0.036s
sys 0m0.024s
$ >/tmp/foo; time for i in `seq 1 3000`; do /usr/bin/printf '%s ' "$i" >> /tmp/foo; done;
real 0m18.097s
user 0m1.048s
sys 0m7.124s
@Guru が指摘しているように、その多くは によってのみ発生するスレッド作成のコストによるものです/usr/bin/printf
。それがすべてであれば、ループの外で実行した場合、実行可能ファイルは組み込みファイルよりも高速になるはずです。残念ながら、/usr/bin/printf
には受け入れ可能な変数のサイズに制限があるため、比較的短い文字列でしかこれをテストできませんでした。
$ i=$(seq 1 28000 | awk '{k=k$1}END{print k}'); time /usr/bin/printf '%s ' "$i" > /dev/null;
real 0m0.035s
user 0m0.004s
sys 0m0.028s
$ i=$(seq 1 28000 | awk '{k=k$1}END{print k}'); time printf '%s ' "$i" > /dev/null;
real 0m0.008s
user 0m0.008s
sys 0m0.000s
組み込みは、依然として一貫して大幅に高速です。さらにわかりやすくするために、両方で新しいプロセスを開始するようにします。
$ time for i in `seq 1 1000`; do /usr/bin/printf '%s ' "$i" >/dev/null; done;
real 0m33.695s
user 0m0.636s
sys 0m30.628s
$ time for i in `seq 1 1000`; do bash -c "printf '%s ' $i" >/dev/null; done;
real 0m3.557s
user 0m0.380s
sys 0m0.508s
私が考えられる唯一の理由は、印刷される変数が内部にありbash
、組み込み関数に直接渡すことができるということです。これで速度の違いを説明するのに十分でしょうか? 他にはどのような要因が関係しているのでしょうか?
答え1
スタンドアロン printf
プロセスを呼び出す際の「コスト」の一部は、リソースを大量に消費するいくつかの処理が発生することです。
- 実行可能ファイルはディスクからロードする必要がありますが、実行可能ファイルが保存されているディスクからバイナリ BLOB をロードするために HDD にアクセスする必要があるため、速度が低下します。
- 実行可能ファイルは通常、動的ライブラリを使用して構築されるため、実行可能ファイルのいくつかのセカンダリ ファイルもロードする必要があります (つまり、HDD から読み取られるバイナリ BLOB データがさらに増えます)。
- オペレーティング システムのオーバーヘッド。呼び出す各プロセスには、プロセス ID を作成する必要があるという形でオーバーヘッドが発生します。また、手順 1 と 2 で HDD からロードされるバイナリ データを格納するためにメモリ内のスペースを確保する必要があります。また、プロセスの環境 (環境変数など) などを格納するために複数の構造を設定する必要があります。
の抜粋/usr/bin/printf
$ strace /usr/bin/printf "%s\n" "hello world"
*execve("/usr/bin/printf", ["/usr/bin/printf", "%s\\n", "hello world"], [/* 91 vars */]) = 0
brk(0) = 0xe91000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a6b000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=242452, ...}) = 0
mmap(NULL, 242452, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd155a2f000
close(3) = 0
open("/lib64/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\357!\3474\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1956608, ...}) = 0
mmap(0x34e7200000, 3781816, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x34e7200000
mprotect(0x34e7391000, 2097152, PROT_NONE) = 0
mmap(0x34e7591000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x191000) = 0x34e7591000
mmap(0x34e7596000, 21688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x34e7596000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a2e000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a2c000
arch_prctl(ARCH_SET_FS, 0x7fd155a2c720) = 0
mprotect(0x34e7591000, 16384, PROT_READ) = 0
mprotect(0x34e701e000, 4096, PROT_READ) = 0
munmap(0x7fd155a2f000, 242452) = 0
brk(0) = 0xe91000
brk(0xeb2000) = 0xeb2000
brk(0) = 0xeb2000
open("/usr/lib/locale/locale-archive", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=99158752, ...}) = 0
mmap(NULL, 99158752, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd14fb9b000
close(3) = 0
fstat(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a6a000
write(1, "hello world\n", 12hello world
) = 12
close(1) = 0
munmap(0x7fd155a6a000, 4096) = 0
close(2) = 0
exit_group(0) = ?*
/usr/bin/printf
上記を見ると、スタンドアロン実行可能ファイルであるために発生する追加リソースを把握できます。
組み込み printf
ビルドされたバージョンでは、printf
Bash が呼び出されたときに、依存するすべてのライブラリとそのバイナリ blob がすでにメモリにロードされています。そのため、再度そのような処理が発生することはありません。
実際には、Bash に組み込みの「コマンド」を呼び出すと、すべてがすでにロードされているため、実際には関数呼び出しを行っていることになります。
類推
Perl などのプログラミング言語を使用したことがある場合、これは関数 ( ) を呼び出すsystem("mycmd")
か、バックティック ( `mycmd`
) を使用するのと同じです。これらのいずれかを実行すると、Perl のコア関数を通じて提供される関数を使用するのではなく、独自のオーバーヘッドを持つ別のプロセスをフォークすることになります。
Linux プロセス管理の解剖
IBM Developerworks に、Linux プロセスの作成と破棄の方法と、そのプロセスに関係するさまざまな C ライブラリをさまざまな側面から分析した非常に優れた記事があります。記事のタイトルは次のとおりです。Linux プロセス管理の解剖 - 作成、管理、スケジュール、破棄. また、PDF。
答え2
外部コマンドを実行すると、/usr/bin/printf
シェル組み込みでは行われないプロセスが作成されます。そのため、3000 のループでは 3000 のプロセスが作成され、速度が低下します。
ループの外で実行することでこれを確認できます。
答え3
新しいプロセスの生成とセットアップ、プログラムとそのライブラリ依存関係のロード、実行、初期化、クリーンアップ、終了にかかる時間が、実際にアクションを実行するために必要な時間をはるかに上回っているという事実はすでに説明しましたが、ここでは、printf
1つの高価なアクションのさまざまな実装でのタイミングをいくつか示します。ない残りのものによって影が薄くなった:
$ time /usr/bin/printf %2000000000s > /dev/null
/usr/bin/printf %2000000000s > /dev/null 13.72s user 1.42s system 99% cpu 15.238 total
$ time busybox printf %2000000000s > /dev/null
busybox printf %2000000000s > /dev/null 1.50s user 0.49s system 95% cpu 2.078 total
$ time bash -c 'printf %2000000000s' > /dev/null
bash -c 'printf %2000000000s' > /dev/null 4.59s user 3.35s system 84% cpu 9.375 total
$ time zsh -c 'printf %2000000000s' > /dev/null
zsh -c 'printf %2000000000s' > /dev/null 1.48s user 0.24s system 81% cpu 2.115 total
$ time ksh -c 'printf %2000000000s' > /dev/null
ksh -c 'printf %2000000000s' > /dev/null 0.48s user 0.00s system 88% cpu 0.543 total
$ time mksh -c 'printf %2000000000s' > /dev/null
mksh -c 'printf %2000000000s' > /dev/null 13.59s user 1.57s system 99% cpu 15.262 total
$ time ash -c 'printf %2000000000s' > /dev/null
ash -c 'printf %2000000000s' > /dev/null 13.74s user 1.42s system 99% cpu 15.214 total
$ time yash -c 'printf %2000000000s' > /dev/null
yash -c 'printf %2000000000s' > /dev/null 13.73s user 1.40s system 99% cpu 15.186 total
少なくともその点では、GNU はprintf
パフォーマンスに最適化されていないことがわかります。 のようなコマンドを最適化することにはあまり意味がありません。なぜなら、99.999% の使用では、アクションの実行に費やされる時間は実行時間によって影を潜めてしまうからです。 またはのような、潜在的にギガバイト単位のデータを 1 秒で処理できるprintf
コマンドを最適化する方がはるかに理にかなっています。grep
sed
1つ走る。