為什麼 bash 的 printf 比 /usr/bin/printf 快?

為什麼 bash 的 printf 比 /usr/bin/printf 快?

我有兩種方式調用printf我的系統:

$ 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

因此,一個是 bash 內建指令,另一個是正確編譯的可執行檔。我本來期望一個程式的唯一工作就是printf比 shell 內建程式快得多。當然,內建函數已經載入到記憶體中,但在專用程式中實際執行時間應該更快,對嗎?它將被優化以在最好的 Unix 哲學中很好地完成一件事。

顯然不是:

$ >/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. 可執行檔必須從磁碟加載,這會導致速度變慢,因為必須存取 HDD 才能從儲存可執行檔的磁碟載入二進位 blob。
  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

當呼叫 Bash 時,它所依賴的所有函式庫的建置版本printf及其二進位 blob 都已載入到記憶體中。因此,這些都不必再發生。

實際上,當您調用 Bash 的內建“命令”時,您實際上是在進行函數調用,因為所有內容都已加載。

打個比方

如果您曾經使用過程式語言,例如 Perl,那麼這相當於呼叫函數 ( system("mycmd")) 或使用反引號 ( `mycmd`)。當您執行這些操作中的任何一個時,您都在分叉一個單獨的進程,並有自己的開銷,而不是使用透過 Perl 核心函數提供給您的函數。

Linux 行程管理剖析

IBM Developerworks 上有一篇非常好的文章,詳細介紹了 Linux 進程如何建立和銷毀的各個方面,以及該進程中涉及的不同 C 程式庫。文章標題為:Linux行程管理剖析-建立、管理、排程與銷毀。它也可以作為PDF

答案2

執行外部命令/usr/bin/printf會導致建立進程,而內建 shell 則不會。因此,對於 3000 個循環,創建了 3000 個進程,因此速度較慢。

您可以透過在循環外運行它們來檢查這一點:

答案3

雖然生成和設置新進程以及加載、執行和初始化、清理和終止程序及其庫依賴項的時間遠遠掩蓋了執行該操作所需的實際時間,但這裡已經涵蓋了這一事實printf對於一項昂貴的行動,有不同的實施時間,即不是被其他的掩蓋了:

$ 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

可以看到,至少在這方面,GNUprintf並沒有針對效能進行最佳化。無論如何,優化指令沒有太大意義,printf因為對於 99.999% 的使用情況,執行操作所花費的時間無論如何都會被執行時間所掩蓋。優化諸如grep或之類的命令更有意義,sed這些命令可能會處理千兆位元組的數據跑步。

相關內容