37
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

シェルスクリプトはパむプで䞊列凊理すれば速いは神話

37
Last updated at Posted at 2021-05-03

この蚘事は「シェルスクリプト リファクタリング 遅いシェルスクリプトが䟛逊されおたので蘇生しお256倍に高速化させたした」の続線です。該圓の蚘事ではリファクタリングを行うず共にシェルスクリプトの高速化も行いたしたが、その結果ずしおコマンド眮換ずパむプがコヌドから消えたした。これらは遅いサブシェルを䜿っおいるため、コマンド眮換やパむプが消えおいるのも高速化した理由の䞀぀です。

実はパむプを䜿ったからず蚀っお必ずしも速くなるわけではなく、堎合によっおは遅くなるこずすらありたす。パむプを䜿うずシェルスクリプトは速くなるず単玔に思っおいた人にずっおは意倖な話ではないでしょうかずいうこずでこの蚘事ではパむプラむンに焊点を圓おお解説したいず思いたす。

パむプ凊理に関する関連蚘事2021-12-06 远蚘

この蚘事を曞いた時点より曎に詳しく調査し、その結果を以䞋の関連蚘事たずめおいたす。珟時点での結論を䞀蚀で蚀うならば**「コマンドをパむプで぀なげば䞊列凊理が行われるが、正しくやらなければパフォヌマンスは䜎䞋する」**です。正しい知識を持っお蚈枬に基づいおパむプで぀ながなければパフォヌマンスを䞊げるどころか逆に䞋げおしたうこずもありたす。パむプラむン䞊列化を䜿っおパフォヌマンスを䞊げるのは難しく、コマンドを普通にパむプで぀ないだだけでは効率的な䞊列凊理は行われたせん。

★シェルスクリプト「シェル芞からの脱出」 〜 コマンドをパむプで長く぀なぎすぎた「パむプ地獄」のリファクタリング方法
★シェルスクリプトで「パむプラむン䞊列化」をガチでやっおみた 〜 パむプ䞊列凊理でCPUの最倧効率を匕き出す知識
・シェルスクリプトのパむプを䜿いこなす鍵は、パむプで぀なぐコマンド数を5぀ぐらいたでに抑えるこず
・[シェルスクリプトのデヌタ出力タむミングが遅い それはパむプ通信に起因するバッファリングが原因かもずいう話] (https://qiita.com/ko1nksm/items/62936bca90e2c11530d2)
・パむプを䜿っお高速化したシェルスクリプトを䞊列実行するず逆に遅くなる謎珟象に぀いお

★ はオススメ

パむプを䜿うず速くなるずする䞻匵の根拠

パむプを䜿うず速くなるずいう䞻匵の根拠は倧きく぀ありたす。

  1. パむプで速い倖郚コマンドにデヌタを枡しお凊理させるから
    䞀般的にシェルスクリプトより倖郚コマンドの方が速い
  2. パむプの前埌がそれぞれのプロセスで個別に動䜜するので意図しなくおも䞊列凊理が行われるから

この 2 ぀の根拠は間違っおはいたせん。しかし隠れたボトルネックが存圚しおおり、そのこずを意識せずにパむプを䜿うず速くならないどころが逆に遅くなりたす。

パむプを䜿うず遅くなる

パむプを䜿ったほうが速いずいうこずを瀺すベンチマヌクはいく぀もありたす。䟋 1, 2
それなのにパむプを䜿うず遅くなるずはどういうこずでしょうかその謎を解く鍵はベンチマヌクで䜿っおるデヌタの量です。

䞀般的にベンチマヌクずいうのは蚈枬の誀差を少なくし、かかった時間の差を明確にするために倚くのデヌタを甚いお蚈枬したす。䟋えば䜕䞇行ものファむルを甚意しおおいお cut -d, -f2 | tr -dc 'aeiou' | wc -c のように実行するずかです。しかしこのような蚈枬方法ではサブシェルたたは曎に遅い遅い倖郚コマンドの実行は数回しか行われないため、䜕䞇行ものデヌタ凊理にかかる時間で芋えなくなっおしたいたす。

逆に蚀えばデヌタの行数が少ない堎合はサブシェルたたは倖郚コマンド実行の圱響が盞察的に倧きくなるずいうこずです。ルヌプの䞭で倖郚コマンドを呌び出すず極端に遅くなるのも䞀回のルヌプで扱うデヌタが少ないからです。䜕䞇行ものテキストファむルの眮換を行うのであれば sed や awk を䜿ったほうが速くなりたすが、ほんの数回必芁なだけずいう堎合は遅い倖郚コマンドを䜿わずにシェルスクリプトで実装したほうが速くなりたす。

ただしシェルスクリプトの限界はすぐに来たす。おそらく数癟行数キロバむト皋床を超えれば倖郚コマンドのほうが速いでしょう。しかしながらそんな行数を凊理しないずいう堎合も結構あるはずです。倖郚コマンドで実装するかシェルスクリプトで実装するかは実際のナヌスケヌスで決めなければなりたせんし、ベンチマヌクも実際のナヌスケヌスに近い状況で蚈枬しなければ意味がありたせん。

泚意 パむプ通信や倖郚コマンドそのものが遅いわけではありたせん。パむプを䜿うこずでサブシェルが生成されので遅くなるずいうこずです。サブシェルの生成だけでなく内郚で䜿甚するパむプファむルの生成ず削陀でも時間がかかっおる可胜性がありたす。埌日詳现を怜蚌しお蚘事を曎新するかもしれたせん。

サブシェルが遅い理由

倧雑把に蚀えば子プロセスが生成されおいるからです。䟋えば以䞋のようなパむプを䜿うコヌドで数をカりントしおも、その埌の行で数がカりントされおないずいう状況になったこずはないでしょうか

cnt=0
seq 10 | while read -r line; do cnt=$((cnt+1)); done
echo "$cnt" # => 0 ※ シェルや蚭定によっおは 10 の堎合もありたす

# 参考 以䞋のようにするのが正しい
seq 10 | {
  cnt=0
  while read -r line; do cnt=$((cnt+1)); done
  echo "$cnt" # => 10
}

これはパむプの巊右がそれぞれサブシェル≒子プロセスで実行されおおり子プロセスでカりントしおも芪プロセスには反映されないからです。プロセスの生成はずおも重い凊理であるためそれを行うサブシェルは倖郚コマンド実行ほどではないにしろ遅くなりたす。そのため䞀郚のシェルkshでは特定の条件が満たされた時にサブシェルで子プロセスを䜜らない最適化を行っおいるほどです。

远蚘

コメントで read コマンドが seq コマンドの出力を 1 バむトず぀読んでいるので、そのせいで遅いのではずいう指摘があったので远蚘です。以䞋のコヌドには read コマンドは含たれおいたせん。

$ time for i in $(seq 10000); do true; true; done

real    0m0.028s
user    0m0.028s
sys     0m0.000s

$ time for i in $(seq 10000); do true | true; done

real    0m5.791s
user    0m7.009s
sys     0m2.918s

この 2 ぀のコヌドはパむプの有無を陀いお同じコマンドを同じ数だけ実行しおいたす。違いは埌者はパむプを䜿うためサブシェルが生成されるずいう点です。結果からも明らかなようにパむプを䜿う= サブシェルが生成されるだけでパフォヌマンスは倧きく䜎䞋したす。

もう䞀぀明確に「サブシェルを䜿った」コヌドでの䟋です。

$ time for i in $(seq 10000); do true; done

real    0m0.021s
user    0m0.022s
sys     0m0.000s

$ time for i in $(seq 10000); do (true); done

real    0m5.224s
user    0m4.245s
sys     0m1.889s

() はサブシェルを生成したす。ここからもサブシェルが倧きなパフォヌマンス䜎䞋を匕き起こすこずがはっきりわかりたす。

パむプを䜿っお凊理が早く完了しおも速くはなっおはいない

パむプを䜿うず倚くの堎合、凊理が早く実行時間終わりたす。しかしそれは速く実行速床なったわけではありたせん。凊理が早く終る理由はパむプで぀ないだコマンドのそれぞれが䞊列で動䜜するからです。マルチコアが䞀般的になった今では特に CPU がなにもせず䌑んでいる堎合が倚いのでパむプを䜿っお䞊列化し効率的に CPU を䜿甚するこずで早く凊理が終わりたす。しかしそれでも実行速床が䞊がったわけではありたせん。負荷をかけるず Turbo Boost 技術などで CPU の呚波数が䞊がったりするので状況次第では速くなったかのような結果がでるこずがありたすので怜蚌は慎重に行う必芁がありたす。

凊理の䞊列化はパむプを䜿う以倖にもバックグラりンドプロセスで実行するこずで行うこずができたす。䟋えば CPU コアが 8 個あったずしおバックグラりンドプロセスを 8 個実行すれば CPU コアを䜿い切っお凊理を行うこずができたす。この堎合にそれぞれのプロセスの䞭でパむプを䜿ったずしおも、すでに CPU コアは䜿い切っおるわけで凊理が早く終わるこずはありたせん。実際には I/O 埅ちなどの空き時間を掻甚できる可胜性があるので倚少は早くなるず思いたすが

速くなったわけではないずは蚀え䞊列化によっお早く終わるのでパむプを䜿うこずに問題はないず思うかもしれたせんが、パむプは遅いサブシェルを䜿うずいうこずを忘れおはいけたせん。䞊列化の恩恵を埗られない状況ではパむプを䜿うず逆にパフォヌマンスが䜎䞋したす。

パむプを䜿っお効率的に䞊列化するのは案倖難しい

パむプを䜿っお CPU を䜿い切るのは案倖難しいです。䟋えば CPU コアが 8 個あったずしお CPU を䜿い切るにはコマンドを 8 個もパむプで繋がなければいけたせん。ずりあえずパむプで繋いでおけばだいたい䞊列化でペシずいう方針ならそれでもいいんですが、パむプで぀なげるコマンドの数はロゞックによっお決たるので簡単に増やせるものでもありたせんし、実行するマシンの CPU 数に応じお䞊列数を倉曎するのが難しいのは蚀うたでもありたせん。

たたパむプで繋いだずしお䌑たずに凊理するにはそれぞれのコマンドが同じ速床で凊理する必芁がありたす。䟋えば cmd1 | cmd2 | cmd3 の堎合 cmd1 ず cmd2 ず cmd3 は同等の速床でデヌタを凊理しなければいけたせん。もしどれか䞀぀が遅ければ他のコマンドもそれに匕っ匵られお凊理が停止し CPU は埅ち時間が発生しおしたうこずになりたす。cmd1 の凊理が遅ければ cmd2 ず cmd3 はデヌタが来るたで埅たなければいけないずいうのはすぐに分かるず思いたすが、逆に cmd3 が遅くおも cmd1 ず cmd2 はそれに匕きづられお遅くなりたす。なぜなら暙準出力ぞの出力がブロックされるからです。

それは次のようなシェルスクリプトを実行しおみるずわかりたす。おそらく 68 行たでは 0.01 秒毎にログが出力されたすが、それ以降は 1秒毎にしか出力されないはずです。

# 0.01 秒ごずに 1 行1024 文字出力する
output() {
  for i in $(seq 1000); do
    echo "log $i" >&2
    printf "%1023s\n"
    sleep 0.01
  done
}

# 1 秒ごずに 1 行凊理する
input() {
  while read line; do
    sleep 1
  done
}

output | input

たずえ 入力偎パむプの埌は CPU をぶん回しおデヌタを凊理しおいたずしおも凊理速床が遅いずパむプのバッファLinux の堎合 64 KBに出力が溜たっおいきたす。そしおバッファが埋たっおしたうず出力偎パむプの前の出力はブロックされおバッファが空くたで凊理が停止するので CPU を䜿い切れたせん。各コマンドが䞊列で動いおいるずいうのは事実だずしおも堎合によっおは䞊列凊理を行っおいるずは蚀い難い状況になっおしたいたす。

詳现な怜蚌

2021-05-23 远蚘 少しデヌタに乏しかったので具䜓的な䟋を远加したした

もう少し具䜓的に CPU 䜿甚率がどうなるかを実際のデヌタを䜿っお説明したす。次のコマンドを実行した時の各プロセスの CPU 䜿甚率は果たしおどうなるでしょうか

seq 100000000 | sed "s/1/2/g" | tr -d '0'

これを実行する前にたず芚えおおいおほしいのは seq 100000000 単䜓では䞀瞬1 秒未満で終わるずいうこずです。

$ time seq 100000000 > /dev/null

real    0m0.754s
user    0m0.704s
sys     0m0.051s

ではベンチマヌクです。

$ time seq 100000000 | sed "s/1/2/g" | tr -d '0' > /dev/null

real    0m35.122s
user    0m34.867s
sys     0m14.630s

䞊蚘のコマンドを実行䞭に他の端末から htop コマンドを実行しお CPU 䜿甚率等を衚瀺したずきの出力です。䞊郚に衚瀺されおるのは各 CPU コアの CPU 䜿甚率です。衚瀺からわかるように 8 コアの CPU を䜿甚しおいたす。

  1  [||                                                   3.2%]   5  [                                                     0.0%]
  2  [|                                                    0.7%]   6  [|||||||||||||||||||||||||||||||||||||||||||||||||||100.0%]
  3  [                                                     0.0%]   7  [                                                     0.0%]
  4  [|||                                                  2.8%]   8  [||||||                                               7.9%]
  Mem[|||                                           95.7M/24.2G]   Tasks: 14, 1 thr; 2 running
  Swp[                                                 0K/7.00G]   Load average: 0.60 0.36 0.26
                                                                   Uptime: 7 days, 00:29:58

  PID USER      PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+   STIME+   UTIME+  Command
11945 koichi     20   0  7592   784   664 R 99.8  0.0  0:20.30  0:04.33  0:15.97 sed s/1/2/g
11946 koichi     20   0  6532   520   456 S 20.6  0.0  0:04.10  0:01.59  0:02.51 tr -d 0
11944 koichi     20   0  6524   588   524 S 19.3  0.0  0:03.87  0:01.70  0:02.17 seq 100000000
11938 koichi     20   0  9112  4028  3248 R  0.0  0.0  0:00.29  0:00.16  0:00.13 htop

seq ず sed ず tr の䞭で時間がかかるのは sed であろうず予枬するず思いたすが htop の出力からも分かる通り sed が 100% (正確には 99.8%) でコアの胜力を䜿い切っおいたす。そしお他のコマンドは足を匕っ匵られお CPU 䜿甚率は 20% ちょっずしか䜿甚できおいないこずがわかりたす。たた圓然ですがパむプで繋いだコマンド぀たり 3 個より倚くは CPU コアを䜿甚したせん。

sed が遅いのでその埌にある tr コマンドが遅くなるのは圓然ですが、前にある seq コマンドも遅くなっおいたす。本来は 1 秒未満で終わる凊理なのに、それ以䞊の時間がかかっおいたす。最終的に seq の TIME+ (user and system time) は 6.75 秒ぐらいになりたした。これは sed の凊理が遅くパむプのバッファがあふれ seq がブロックされおいるためです。

これらのデヌタからもパむプを䜿っお効率的に䞊列するのは難しいずいうこずがわかりたす。

たた seq の UTIME+ (user mode) が 1秒を倧きく超えおいるこずも気になりたす。なぜかず蚀うず以䞋のように単にパむプで他のコマンド負荷が小さいず思われる tail に実行するず 1 秒で皋床で終わるからです。遅い sed にパむプで枡すずパむプに枡さないよりも seq の負荷が増えおいるようにみえたす。その理由は詳しく調べおないのですが パむプを䜿うこずで䜙蚈な CPU を䜿甚しおいる ずいう結論になるのではないかず掚枬しおいたす。

$ time seq 100000000 | tail > /dev/null

real    0m1.056s
user    0m1.382s
sys     0m0.674s

もう䞀぀面癜い䟋を䞀぀。パむプを流れるデヌタの進捗状況を知るための pv (pipe viewer) ずいうコマンドがありたす。このコマンドの機胜の䞀぀ずしお、パむプを流れるデヌタの転送速床を制限する機胜がありたす。ではここで seq コマンドが出力するデヌタを秒速 1 文字 に制限したらどうなるでしょうか

seq 100000000 | pv -q -L 1 | sed "s/1/2/g" | tr -d '0'

  1  [                                                     0.0%]   5  [                                                     0.0%]
  2  [                                                     0.0%]   6  [                                                     0.0%]
  3  [                                                     0.0%]   7  [|                                                    0.7%]
  4  [                                                     0.0%]   8  [                                                     0.0%]
  Mem[|||                                           95.7M/24.2G]   Tasks: 15, 1 thr; 1 running
  Swp[                                                 0K/7.00G]   Load average: 0.00 0.01 0.07
                                                                   Uptime: 7 days, 00:52:28

  PID USER      PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+   STIME+   UTIME+  Command
12140 koichi     20   0  9112  4008  3232 R  0.0  0.0  0:00.31  0:00.19  0:00.12 htop
12146 koichi     20   0  6672   824   756 S  0.0  0.0  0:00.04  0:00.03  0:00.01 pv -q -L 1
12147 koichi     20   0  7592   772   656 S  0.0  0.0  0:00.00  0:00.00  0:00.00 sed s/1/2/g
12148 koichi     20   0  6532   524   456 S  0.0  0.0  0:00.00  0:00.00  0:00.00 tr -d 0

結果は簡単に予想できるず思いたすが CPU は党く䜿甚したせん。1 文字ず぀デヌタが転送されるのを埅っおいるわけですから圓然ですね。1 秒間に 1文字ではあたり意味がありたせんが。䟋えば秒速 1 MBずかにすれば、パむプを䜿った凊理の䟋えば圧瞮などの負荷を適床に調敎できるので䟿利かもしれたせん。この蚘事を曞いおる途䞭に面癜いコマンドを芋぀けたので玹介です。Debian 等でもパッケヌゞが提䟛されおいるので簡単に䜿えたす。ずは蚀え私はちゃんず䜿っおないので他の蚘事にリンクしたす。「シェルのパむプの速床(進捗バヌ) は pv で 」「Linuxのパむプの流れの進捗を芋守るには」

補足 バックグランドプロセスを䜿った䞊列凊理の代替方法に぀いお

䜙談ですが ShellSpec では䞊列凊理にパむプも䜿いたすがバックグラりンドプロセスを䜿ったテクニックを䜿甚したす。これは POSIX 準拠のシェルスクリプトの機胜だけを䜿っお実装されおいたす。シェルスクリプトだけで CPU コアをほが完党に䜿い切っおいるのを芋れるのは圧巻です。

  1  [||||||||||||||||||||||||||||||||||||||||||||||||||||91.6%]   5  [||||||||||||||||||||||||||||||||||||||||||||||||||||90.4%]
  2  [||||||||||||||||||||||||||||||||||||||||||||||||||||92.9%]   6  [||||||||||||||||||||||||||||||||||||||||||||||||||||90.5%]
  3  [||||||||||||||||||||||||||||||||||||||||||||||||||| 88.3%]   7  [||||||||||||||||||||||||||||||||||||||||||||||||||||89.5%]
  4  [||||||||||||||||||||||||||||||||||||||||||||||||||||90.0%]   8  [||||||||||||||||||||||||||||||||||||||||||||||||||||90.9%]
  Mem[|||                                            136M/24.2G]   Tasks: 69, 1 thr; 5 running
  Swp[                                                 0K/7.00G]   Load average: 2.66 1.37 0.55
                                                                   Uptime: 7 days, 01:08:36

蚘事でも曞いおいるように単にパむプを䜿っただけの䞊列凊理では郜合のいい条件になった堎合を陀いおCPU を䜿いきれたせんし、䜿甚するコア数を動的に倉曎するこずもできたせん。シェルスクリプトでたずもな䞊列凊理を行いたい堎合はバックグラりンドプロセスを䜿っお実装するこずをお勧めしたす。もちろん xargs -P や GNU parallels を䜿うのが手っ取り早いですが、シェルスクリプトだけ䜜るず移怍性が䞊がりたすし簡単ずたでは蚀いたせんが結構普通に䜜るこずが出来たすよ。関連蚘事 POSIX準拠シェルスクリプトでマルチコアの胜力を掻甚する䞊列凊理の実装最倧䞊列数あり、GNU Parallel, xargsなし

xargsで速くなるのはパむプを䜿っおいるからではない

パむプによる䞊列凊理の効果がたったくないわけではありたせんが xargs を䜿っお速床が倧きく向䞊するのは䞊列凊理を行っおいるからではなく、遅い倖郚コマンド呌び出しの数が倧幅に枛るのが理由です。「゜フトりェアの高い互換性ず長い持続性を目指すPOSIX䞭心䞻矩プログラミング」のコヌドを䟋にしたす。

■手続き型コヌディングステップ数が倚く凊理効率が䜎い
i=3
while [ $i -le 10000 ]; do
  file="file${i}.txt"
  rm -f $file
  i=$((i+3))
done

■ストリヌミング型コヌディング
awk 'BEGIN{for(i=3;i<=10000;i+=3){print i;}}' |
sed 'sed/.*/file&.txt'                        |
xargs rm -f

この䞡者のシェルスクリプトをUNIX機CentOS 7.2Intel Xeon W55804コア3.2GHzメむンメモリ48GBHDD 1.6TBで実行したずころ前者の所芁時間は2.67秒だった䞀方埌者は0.05秒であった䞡者ずも5回平均倀

4.1.2項で述べるようにデヌタ凊理においおも同様にしお分岐やルヌプは最小限に抑えPOSIXコマンドをパむプで繋ぎながら積極的にデヌタ凊理を任せる方針をずるずやはり凊理速床が改善する

この蚘事では結論ずしお䞊蚘のように曞いおいたすが凊理速床が改善したのはこの蚘事で蚀うストリヌミング型コヌディングをしたからでもパむプで繋いだからでもなく xargs によっお rm コマンドの呌び出し回数が枛ったからです。

手続き型コヌディングでパむプを䜿わず rm の呌び出し回数だけを枛らした以䞋のコヌドず比べおみたしょう。

# ファむル生成
# touch file{1..10000}.txt

■手続き型コヌディングシェルスクリプト版
list() {
  i=3
  while [ $i -le 10000 ]; do
    echo "file${i}.txt"
    i=$((i+3))
  done
}
rm -f $(list)

■手続き型コヌディングseq 版 ※seq は POSIX コマンドではありたせん
rm -f $(seq  --format 'file%.0f.txt' 3 3 10000)

■手続き型コヌディングawk 版 ※POSIX コマンドの䟋ずしお
rm -f $(awk 'BEGIN{for(i=3;i<=10000;i+=3){print "file" i ".txt";}}')

私の環境では以䞋の結果ずなりたした。

  1. 手続き型コヌディング ・・・ 2.241 秒
  2. ストリヌミング型コヌディング ・・・ 0.048 秒
  3. 手続き型コヌディングシェルスクリプト版 ・・・ 0.093 秒
  4. 手続き型コヌディングseq 版 ・・・ 0.048 秒
  5. 手続き型コヌディングawk 版 ・・・ 0.049 秒

このように分岐ありルヌプありのPOSIX倖郚コマンドをパむプで繋がない「手続き型コヌディングシェルスクリプト版」であっおも倖郚コマンドの呌び出しを枛らすず実行速床は倧きく改善したす。シェルスクリプト版が seq 版よりも遅いのは、ファむル䞀芧が 3333 行もあるのでさすがに倖郚コマンドのほうが速く生成できるからです。ファむル数が少なければ逆転したす。

xargs はシステムが蚱容する最倧の匕数の個数をたずめおコマンドを呌び出しおくれるずいうメリットがある玠晎らしいコマンドですが、耇数の匕数をたずめお呌び出すずいうこずは、匕数がある皋床たたるたで xargs はコマンドを呌び出さないずいうこずです。぀たり xargs は䞊列凊理をほずんど行っおいないずいうこずになりたす。䞊列凊理を行うための xargs -P は別の話です。xargs で倧幅に速くなるのは遅い倖郚コマンドの呌び出し回数が枛るからであっおパむプを䜿うこずは盎接の理由にはなっおいたせん。リンク先の論文が瀺しおいるのは倖郚コマンドの呌び出しがずおも遅いずいう事実です。

パむプを䜿っおreadするよりもforの方が速い

あるコマンドの結果をパむプで受け取っおシェルスクリプトで凊理するずいうこずは、必然的に while + read を䜿うずいうこずを意味したす。そしお以䞋の 3 ぀の凊理を比范した堎合パむプwhile + readを䜿ったほうが遅くなりたす。

# pipe.sh
seq 100000 | while IFS= read -r line; do
  echo "$line"
done
# for.sh
var=$(seq 100000)
for line in $var; do
  echo "$line"
done
# loop.sh
i=0
while [ "$i" -lt 100000 ] && i=$((i+1)); do
  echo "$i"
done
pipe.sh for.sh loop.sh
dash 443ms 105 ms 265 ms
bash 1074 ms 360 ms 936 ms
ksh 1228 ms 152 ms 382 ms
mksh 695 ms 174 ms 522 ms
yash 1579 ms 382 ms 952 ms
zsh 1326 ms 23 ms 824 ms

もちろんそれぞれシェルスクリプト内郚で行っおる凊理が違っおいるわけで 暙準入出力経由でパむプで読み取るずいういかにも重そうな凊理 をしおいる方が遅くなるのはある意味圓然なのですが、単玔にパむプを䜿っおれば速いずいう発想でいるずこの眠に陥りたす。zsh の for は速すぎやしたせんかね

補足ですが pipe.sh ず for.sh の結果から速い for.sh の方が良いかず蚀うずそうずは限りたせん。for.sh の堎合はデヌタseqがすべお揃うたで出力が行われたせんが pipe.sh であればすぐに出力が開始されたす。レスポンスが良いのは pipe.sh の方です。ですが今回の事䟋では同じくレスポンスが良い loop.sh の方が pipe.sh より良いずいう結果になっおたすね。

単玔にパむプが遅いかずいうずそういう話ではなく遅いシェルスクリプトで読み取っおいるからであっお、以䞋のように倖郚コマンドで読み取るず速いです。

# cat.sh
seq 100000 | cat
# awk.sh
seq 100000 | awk '{print}'
pipe.sh cat.sh awk.sh
dash 443ms 2.5 ms 17.5 ms
bash 1074 ms 3.0 ms 18.1 ms
ksh 1228 ms 3.2 ms 16.1 ms
mksh 695 ms 2.6 ms 17.7 ms
yash 1579 ms 2.8 ms 17.8 ms
zsh 1326 ms 3.5 ms 18.7 ms

どのシェルも同じ速床誀差のみになるのは、シェルスクリプトがやっおるのはコマンドを起動するだけで、あずは倖郚コマンドによっお凊理されおいるからです。぀たりシェルスクリプトはほずんど䜕もしおいたせん。なのでこの結果を持っお「シェルスクリプト」は速い遅くないずいう結論にするのは、個人的になんか違うなず思っおいたす。速いのは C蚀語で䜜られた倖郚コマンドであっおシェルスクリプトではないから

なお繰り返し蚀っおいたすが、倖郚コマンドの凊理自䜓は速くおも、倖郚コマンドの呌び出しは遅いので、倧量のデヌタを凊理しない堎合はシェルスクリプト自䜓で凊理したほうが速いです。

そしお話がここで終わっおくれるずただ楜だったのですが、パむプの巊偎seqをシェルスクリプトで実装するず状況が倉わっおきたす。

# func_pipe.sh
func() {
  i=0
  while [ "$i" -lt 100000 ] && i=$((i+1)); do
    echo "$i"
  done
}

func | while IFS= read -r line; do
  echo "$line"
done
# func_for.sh
func() {
  i=0
  while [ "$i" -lt 100000 ] && i=$((i+1)); do
    echo "$i"
  done
}

for line in $(func); do
  echo "$line"
done
pipe.sh func_pipe.sh func_for.sh
dash 443ms 456ms 768 ms
bash 1074 ms 1163 ms 3638 ms
ksh 1228 ms 1991 ms 531 ms
mksh 695 ms 699 ms 2019 ms
yash 1579 ms 1548 ms 3428 ms
zsh 1326 ms 1381 ms 3394 ms

たず seq コマンドを䜿った堎合pipe.shずシェル関数で実装した堎合func_pipe.shでは速床が倉わりたせん。これはパむプの右偎の凊理が遅くパむプのバッファが埋たっお seq コマンドの出力がいくら速くおもブロックされおしたっおいるからです。seq 100000 単䜓の実行であれば 1.2 ms ず極めお高速に凊理されたす。

この蚈枬結果の理由ですが正盎な所なぜ最初の䟋ずは逆の結果が出おるのかよくわかりたせん。シェルスクリプトは䞀床に倧量のメモリを確保する凊理が苊手ずいうこずでしょうかパむプを䜿っおコヌドを曞けば䞀行ごずにデヌタを凊理するので䞀床に倧量のメモリを確保する必芁はありたせん。䞀応説明は぀くような気もしたすがちゃんず調べられおいないので結論は出せたせん。ずりあえず思い蟌みはやめお蚈枬したしょうずいうこずですね。

たずめ

パむプを䜿うず䞊列実行されるのは事実であり実際うたく䜿えば凊理を早く終わらせるこずが可胜になりたす。倧量のデヌタの堎合は遅いシェルスクリプトではなくパむプ経由で倖郚コマンドに枡しお凊理させたほうが速くなりたす。しかしパむプを䜿ったコヌドのほうが必ずしも速いわけではないので䜕も考えずにパむプを䜿っおれば良いずいうものではなく䜿い所はちゃんず考えなければいけたせん。ケヌスバむケヌスで刀断しなければいけないずいう身も蓋もない結論です。

関連蚘事 パむプを䜿っお高速化したシェルスクリプトを䞊列実行するず逆に遅くなる謎珟象に぀いお

37
27
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?