この記事は「シェルスクリプト リファクタリング ~遅いシェルスクリプトが供養されてたので蘇生して256倍に高速化させました~」の続編です。該当の記事ではリファクタリングを行うと共にシェルスクリプトの高速化も行いましたが、その結果としてコマンド置換とパイプがコードから消えました。これらは遅いサブシェルを使っているため、コマンド置換やパイプが消えているのも高速化した理由の一つです。
実はパイプを使ったからと言って必ずしも速くなるわけではなく、場合によっては遅くなることすらあります。パイプを使うとシェルスクリプトは速くなると単純に思っていた人にとっては意外な話ではないでしょうか?ということでこの記事ではパイプラインに焦点を当てて解説したいと思います。
パイプ処理に関する関連記事(2021-12-06 追記)
この記事を書いた時点より更に詳しく調査し、その結果を以下の関連記事まとめています。現時点での結論を一言で言うならば**「コマンドをパイプでつなげば並列処理が行われるが、正しくやらなければパフォーマンスは低下する」**です。正しい知識を持って計測に基づいてパイプでつながなければパフォーマンスを上げるどころか逆に下げてしまうこともあります。パイプライン並列化を使ってパフォーマンスを上げるのは難しく、コマンドを普通にパイプでつないだだけでは効率的な並列処理は行われません。
★シェルスクリプト「シェル芸からの脱出」 〜 コマンドをパイプで長くつなぎすぎた「パイプ地獄」のリファクタリング方法
★シェルスクリプトで「パイプライン並列化」をガチでやってみた 〜 パイプ+並列処理でCPUの最大効率を引き出す知識
・シェルスクリプトのパイプを使いこなす鍵は、パイプでつなぐコマンド数を5つぐらいまでに抑えること!
・[シェルスクリプトのデータ出力タイミングが遅い? それはパイプ通信に起因するバッファリングが原因かもという話] (https://qiita.com/ko1nksm/items/62936bca90e2c11530d2)
・パイプを使って高速化したシェルスクリプトを並列実行すると逆に遅くなる謎現象について
★ はオススメ
パイプを使うと速くなるとする主張の根拠
パイプを使うと速くなるという主張の根拠は大きく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";}}')
私の環境では以下の結果となりました。
- 手続き型コーディング ・・・ 2.241 秒
- ストリーミング型コーディング ・・・ 0.048 秒
- 手続き型コーディング(シェルスクリプト版) ・・・ 0.093 秒
- 手続き型コーディング(seq 版) ・・・ 0.048 秒
- 手続き型コーディング(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 と極めて高速に処理されます。
この計測結果の理由ですが正直な所なぜ最初の例とは逆の結果が出てるのかよくわかりません。シェルスクリプトは一度に大量のメモリを確保する処理が苦手ということでしょうか?パイプを使ってコードを書けば一行ごとにデータを処理するので一度に大量のメモリを確保する必要はありません。一応説明はつくような気もしますがちゃんと調べられていないので結論は出せません。とりあえず思い込みはやめて計測しましょうということですね。
まとめ
パイプを使うと並列実行されるのは事実であり実際うまく使えば処理を早く終わらせることが可能になります。大量のデータの場合は遅いシェルスクリプトではなくパイプ経由で外部コマンドに渡して処理させたほうが速くなります。しかしパイプを使ったコードのほうが必ずしも速いわけではないので何も考えずにパイプを使ってれば良いというものではなく使い所はちゃんと考えなければいけません。ケースバイケースで判断しなければいけないという身も蓋もない結論です。