tail file | tr(パイプ)が複数行のsedまたはperlよりも速いのはなぜですか?

tail file | tr(パイプ)が複数行のsedまたはperlよりも速いのはなぜですか?

次のような約100万行のファイルがあります。

"ID" "1" "2"
"00000687" 0 1
"00000421" 1 0
"00000421" 1 0
"00000421" 1 0

最後の行は百万回以上繰り返された。 ~からインスピレーションを得るこの問題、提案された解決策のいくつかを試して、どちらが速いかを確認しました。 1つのプロセスしか使用しないソリューションは、パイプがあるソリューションよりも高速であると期待しています。しかし、これは私のテストの結果です。

  • tail -n +2 file.txt | tr -d \"

    $ time tail -n +2 file.txt | tr -d \" 1> /dev/null
    
    real    0m0,032s
    user    0m0,020s
    sys     0m0,028s
    
  • sed '1d;s/"//g' file.txt

    $ time sed '1d;s/"//g' file.txt 1> /dev/null
    
    real    0m0,410s
    user    0m0,399s
    sys     0m0,011s
    
  • perl -ne ' { s/"//g; print if $. > 1 }' file.txt

    $ time perl -ne ' { s/"//g; print if $. > 1 }' file.txt 1> /dev/null
    
    real    0m0,379s
    user    0m0,367s
    sys     0m0,013s
    

何度もテストを繰り返しましたが、いつも同様の数値が出ました。ご覧のとおり、tail -n +2 file.txt | tr -d \"はいはるかに早く他人より。なぜ?

答え1

これは、実行されるタスクの量によって決まります。

あなたのtail | trコマンドは次のように終了します。

  • 存在するtail:
    • 改行まで読みます。
    • 改行の有無にかかわらず、残りの内容をすべて出力します。
  • in はtr読み、改行文字を気にせず、 '''(固定文字)を除くすべてを出力します。

sed与えられたスクリプトを解釈した後、コマンドは次のことを行います。

  • 改行まで読み、入力を累積します。
  • 最初の行の場合は削除してください。
  • 正規表現を解釈した後、すべての二重引用符を空白スペースに置き換えます。
  • 処理されたラインを出力します。
  • ファイルの最後まで繰り返します。

与えられたスクリプトを解釈した後、Perlコマンドは次のことを行います。

  • 改行まで読み、入力を累積します。
  • 正規表現を解釈した後、すべての二重引用符を空白スペースに置き換えます。
  • 最初の行でない場合は、処理された行を出力します。
  • ファイルの最後まで繰り返します。

大量の入力のため、改行文字を見つけるのに費用がかかります。

答え2

これは主にPerlとsedが各行を個別に処理するためです。

Perlが入力を大きな塊として扱い、少し単純化すると(コメントを参照)、より速くすることができますが、trほど高速ではありません。

time perl -ne ' { s/"//g; print if $. > 1 }' file.txt 1> /dev/null

real    0m0.617s
user    0m0.612s
sys     0m0.005s

time perl -pe 'BEGIN{<>;$/=\40960} s/"//g' file.txt >/dev/null

real    0m0.186s
user    0m0.177s
sys     0m0.009s

time tail -n +2 file.txt | tr -d \" 1> /dev/null

real    0m0.033s
user    0m0.031s
sys     0m0.023s

perl -ne '... if $. > 1'注:またはを使用しないでくださいawk 'NR == 1 { ... } /foo/ { ... }'

BEGIN{<>}代わりにとを使用してくださいBEGIN{getline}

最初の行を読んだ後、次の行がもはや最初の行ではないことを確認できます。もう一度確認する必要はありません。

答え3

tail.c の tail_lines():

      /* Use file_lines only if FD refers to a regular file for
         which lseek (... SEEK_END) works.  */

      if ( ! presume_input_pipe
           && S_ISREG (stats.st_mode)
           && (start_pos = lseek (fd, 0, SEEK_CUR)) != -1
           && start_pos < (end_pos = lseek (fd, 0, SEEK_END)))

ここでend_pos = lseek (fd, 0, SEEK_END)ファイルの内容をスキップします。 file_lines() には改行の数を計算するリバーススキャンがあります。

lseek()は、読み取り/書き込みのためにファイルオフセットを再配置するために使用される非常に単純なシステムコールです。


ああ、質問の微妙さを逃したようです。 ;) 1行ずつ読み、1ブロックずつ読み込むことがすべてです。複数のチャンネルを 1 つの複雑なチャンネルに組み合わせるのが最善のケースです。ただし、ここのアルゴリズムには最初の改行文字のみが必要です。

Ole の 2 つの部分で構成される Perl スクリプトは、sysread()最初の改行文字検索から最大のチャンク読み取りに切り替える方法を示しています。

通常どおりに戻るときは、tail最後のブロックを読み、改行を計算します。そこから2番目から最後のブロックまで印刷または読み取ります。

答え4

ぜひ使ってみたいのですがperl遅すぎますね。

perlは一般的なツールですが、次のようにtr近づくことができます。

$ tail -n +2 file.txt | tr -d \" >/dev/null;
real    0m0.040s
user    0m0.030s
sys     0m0.032s

$ perl -e 'while(sysread(STDIN,$b,1)) {$b eq "\n" and last}
           while(sysread(STDIN,$b,131072)) {
             $b=~tr/\"//d; print $b
           }' < file.txt > /dev/null;
real    0m0.049s
user    0m0.045s
sys     0m0.004s

避けてtailより早く行くことができます。

$ time (read; tr -d \") < file.txt >/dev/null
real    0m0.033s
user    0m0.021s
sys     0m0.012s

関連情報