bash の printf が /usr/bin/printf よりも高速なのはなぜですか?

bash の printf が /usr/bin/printf よりも高速なのはなぜですか?

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

プロセスを呼び出す際の「コスト」の一部は、リソースを大量に消費するいくつかの処理が発生することです。

  1. 実行可能ファイルはディスクからロードする必要がありますが、実行可能ファイルが保存されているディスクからバイナリ BLOB をロードするために HDD にアクセスする必要があるため、速度が低下します。
  2. 実行可能ファイルは通常、動的ライブラリを使用して構築されるため、実行可能ファイルのいくつかのセカンダリ ファイルもロードする必要があります (つまり、HDD から読み取られるバイナリ BLOB データがさらに増えます)。
  3. オペレーティング システムのオーバーヘッド。呼び出す各プロセスには、プロセス 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

ビルドされたバージョンでは、printfBash が呼び出されたときに、依存するすべてのライブラリとそのバイナリ blob がすでにメモリにロードされています。そのため、再度そのような処理が発生することはありません。

実際には、Bash に組み込みの「コマンド」を呼び出すと、すべてがすでにロードされているため、実際には関数呼び出しを行っていることになります。

類推

Perl などのプログラミング言語を使用したことがある場合、これは関数 ( ) を呼び出すsystem("mycmd")か、バックティック ( `mycmd`) を使用するのと同じです。これらのいずれかを実行すると、Perl のコア関数を通じて提供される関数を使用するのではなく、独自のオーバーヘッドを持つ別のプロセスをフォークすることになります。

Linux プロセス管理の解剖

IBM Developerworks に、Linux プロセスの作成と破棄の方法と、そのプロセスに関係するさまざまな C ライブラリをさまざまな側面から分析した非常に優れた記事があります。記事のタイトルは次のとおりです。Linux プロセス管理の解剖 - 作成、管理、スケジュール、破棄. また、PDF

答え2

外部コマンドを実行すると、/usr/bin/printfシェル組み込みでは行われないプロセスが作成されます。そのため、3000 のループでは 3000 のプロセスが作成され、速度が低下します。

ループの外で実行することでこれを確認できます。

答え3

新しいプロセスの生成とセットアップ、プログラムとそのライブラリ依存関係のロード、実行、初期化、クリーンアップ、終了にかかる時間が、実際にアクションを実行するために必要な時間をはるかに上回っているという事実はすでに説明しましたが、ここでは、printf1つの高価なアクションのさまざまな実装でのタイミングをいくつか示します。ない残りのものによって影が薄くなった:

$ 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コマンドを最適化する方がはるかに理にかなっています。grepsed1つ走る。

関連情報