Shell

為什麼 echo 和 cat 的執行時間會有這麼大的差異?

  • August 21, 2018

回答這個問題讓我問了另一個問題:

我認為以下腳本做同樣的事情,第二個應該更快,因為第一個使用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是一個龐大的過程。第二個腳本也有一些問題,例如輸出文件的大小低於預期。

我還檢查了手冊頁echocat進行了比較:

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在它接收的參數中擴展序列,這意味著解析內容以及可能的另一個數據分配和副本。

另一方面,好的,在大多數 shellcat中不是內置的,所以這意味著分叉一個程序並執行它(因此載入程式碼和庫),但是在第一次呼叫之後,該程式碼和輸入文件的內容將被記憶體在記憶體中。另一方面,不會有中介。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在循環中使用更有效(除非輸入非常小)。

引用自:https://unix.stackexchange.com/questions/230206