為什麼 echo 和 cat 的執行時間會有這麼大的差異?
回答這個問題讓我問了另一個問題:
我認為以下腳本做同樣的事情,第二個應該更快,因為第一個使用
cat
需要反复打開文件,但第二個只打開文件一次然後只是回顯一個變數:(有關正確程式碼,請參閱更新部分。)
第一的:
#!/bin/sh for j in seq 10; do cat input done >> output
第二:
#!/bin/sh i=`cat input` for j in seq 10; do echo $i done >> output
而輸入大約是 50 兆字節。
但是當我嘗試第二個時,它太慢了,因為回顯變數
i
是一個龐大的過程。第二個腳本也有一些問題,例如輸出文件的大小低於預期。我還檢查了手冊頁
echo
並cat
進行了比較:echo - 顯示一行文本
cat - 連接文件並在標準輸出上列印
但我沒有得到區別。
所以:
- 為什麼 cat 在第二個腳本中如此之快而 echo 如此之慢?
- 還是變數的問題
i
?(因為在它的手冊頁中echo
說它顯示*“一行文本”*,所以我猜它只針對短變數進行優化,而不是針對非常長的變數,例如i
。但是,這只是一個猜測。)- 為什麼我在使用時會遇到問題
echo
?更新
我用
seq 10
而不是seq 10
不正確。這是編輯後的程式碼:第一的:
#!/bin/sh for j in `seq 10`; do cat input done >> output
第二:
#!/bin/sh i=`cat input` for j in `seq 10`; do echo $i done >> output
(特別感謝roaima。)
然而,這不是問題的關鍵。即使循環只發生一次,我也會遇到同樣的問題:
cat
工作速度比echo
.
這裡有幾件事需要考慮。
i=`cat input`
可能很昂貴,並且外殼之間有很多變化。
這是一個稱為命令替換的功能。這個想法是將命令的整個輸出減去尾隨換行符儲存到
i
記憶體中的變數中。為此,shell 在子 shell 中派生命令,並通過管道或套接字對讀取其輸出。你在這裡看到很多變化。在這裡的一個 50MiB 文件上,我可以看到例如 bash 比 ksh93 慢 6 倍,但比 zsh 稍快,比
yash
.慢的主要原因
bash
是它一次從管道讀取 128 個字節(而其他 shell 一次讀取 4KiB 或 8KiB)並且受到系統呼叫成本的懲罰。
zsh
需要做一些後處理來轉義 NUL 字節(其他 shell 在 NUL 字節上中斷),並yash
通過解析多字節字元進行更繁重的處理。所有 shell 都需要去除尾隨換行符,它們可能或多或少地有效。
有些人可能希望比其他人更優雅地處理 NUL 字節並檢查它們的存在。
然後,一旦您在記憶體中擁有那個大變數,對其進行的任何操作通常都涉及分配更多記憶體和應對數據。
在這裡,您將(打算將)變數的內容傳遞給
echo
.幸運的是,
echo
它內置在您的 shell 中,否則執行可能會因arg list too long錯誤而失敗。即使這樣,建構參數列表數組也可能涉及複製變數的內容。命令替換方法中的另一個主要問題是您正在呼叫split+glob 運算符(通過忘記引用變數)。
為此,shell 需要將字元串視為字元串*(*儘管有些 shell 在這方面沒有並且有問題),因此在 UTF-8 語言環境中,這意味著解析 UTF-8 序列(如果還沒有這樣做的話
yash
) , 在字元串中查找$IFS
字元。如果$IFS
包含空格、製表符或換行符(預設情況下是這種情況),則算法更加複雜和昂貴。然後,需要分配和複製由該拆分產生的單詞。glob 部分將更加昂貴。如果這些單詞中的任何一個包含全域字元(
*
,?
,[
),那麼 shell 將不得不讀取某些目錄的內容並進行一些昂貴的模式匹配(bash
例如,眾所周知,它的實現非常糟糕)。如果輸入包含類似的內容
/*/*/*/../../../*/*/*/../../../*/*/*
,那將非常昂貴,因為這意味著列出數千個目錄並且可以擴展到數百 MiB。然後
echo
通常會做一些額外的處理。一些實現\x
在它接收的參數中擴展序列,這意味著解析內容以及可能的另一個數據分配和副本。另一方面,好的,在大多數 shell
cat
中不是內置的,所以這意味著分叉一個程序並執行它(因此載入程式碼和庫),但是在第一次呼叫之後,該程式碼和輸入文件的內容將被記憶體在記憶體中。另一方面,不會有中介。cat
將一次讀取大量數據並立即寫入而不進行處理,並且不需要分配大量記憶體,只需分配一個可以重用的緩衝區。這也意味著它更可靠,因為它不會阻塞 NUL 字節並且不會修剪尾隨換行符(並且不會拆分 + glob,儘管您可以通過引用變數來避免這種情況,並且不會擴展轉義序列,儘管您可以通過使用
printf
而不是echo
) 來避免這種情況。如果你想進一步優化它,而不是
cat
多次呼叫,只需傳遞input
幾次到cat
.yes input | head -n 100 | xargs cat
將執行 3 個命令而不是 100 個。
為了使變數版本更可靠,您需要使用
zsh
(其他 shell 無法處理 NUL 字節)並執行此操作:zmodload zsh/mapfile var=$mapfile[input] repeat 10 print -rn -- "$var"
如果您知道輸入不包含 NUL 字節,那麼您可以通過以下方式可靠地執行 POSIXly (儘管它可能無法在
printf
未內置的地方工作):i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst) n=10 while [ "$n" -gt 10 ]; do printf %s "$i" n=$((n - 1)) done
但這永遠不會比
cat
在循環中使用更有效(除非輸入非常小)。