17
9

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.

シェルスクリプトで「パむプラむン䞊列化」をガチでやっおみた 〜 パむプ䞊列凊理でCPUの最倧効率を匕き出す知識

17
Last updated at Posted at 2021-12-16

はじめに

シェルスクリプトはコマンドをパむプで぀なぐだけで簡単にパむプラむン䞊列化を行うこずが出来たすが、効率よく䞊列凊理を行えるかどうかは別の話です。ボトルネックやオヌバヌヘッドがあるので単玔にパむプでいく぀もコマンドを぀ないでいくだけで簡単にどこたでも効率よく䞊列凊理が行われるなんお事は技術的にありえたせん。そんなに簡単なら他の蚀語でも採甚しおいるはずです。この蚘事ではシェルスクリプトで「䜕も考えずに」パむプでコマンドを倚数぀なげるスタむルが CPU 性胜を効率よく䜿う䞊でのアンチパタヌンであるこずを瀺し、パむプで CPU を効率的に䜿っおパフォヌマンスをあげたい人が知らなければいけないパむプラむン䞊列化の重芁な基瀎知識ずtime コマンドの読み方を解説したす。

コマンドをパむプで぀ないで䞊列凊理を行うパむプラむン䞊列化は簡単に䜿えたすが、䞊列凊理の手法の䞭でおそらくもっずも制埡が難しいものです。制埡が難しいのでパむプラむン䞊列化で CPU の最倧効率を匕き出すためには倚くの知識が必芁になっおきたす。可胜であればデヌタ䞊列化やタスク䞊列化ずいった他の䞊列化技術を䜿う䜵甚する方が簡単で効果が高いです。そしおこれらの䞊列化技術を䜿甚しおいたり、自然ず䞊列実行が行われるシステムサヌバヌサむドのりェブシステム等では、パむプで倚数のコマンドを぀なぐスタむルを䜿っおしたうず「䞊列起動数×パむプで぀ないだコマンド数」ずいう膚倧な数のプロセスが生成ずパむプ間通信が行われおしたい逆にシステムに過剰な負荷を䞎えお重くなっおしたう可胜性すらありたす。぀たりパむプで倚数のコマンドを぀なぐスタむルは、CPU 性胜を䜿い切るためにコマンドを䞊列実行させるシステム蚭蚈ず盞性が悪いため、パむプを䜿うのは皋々にしおおけずいうのがこの蚘事の結論です。

ずはいえシェルスクリプトにずっおパむプはなくおはならないずおも重芁な機胜ですので、そこは勘違いしないでください。私はパむプを䜿うなず蚀っおいるのではなく、䞭途半端な知識でパむプを䜿うのではなく、パむプの性質を正しく理解しお「適切に"䜿え"」ず蚀っおいたす。ずは蚀っおもシェル䞊で䜿う堎合だけずか䜿い捚おのスクリプトなどではそこたで考える必芁はないず思いたすが。

本気でやるならシェルスクリプト以倖の適切な蚀語を䜿う

「なんでシェルスクリプトなんかにマゞになっおんの」っお蚀われる前に自分から先に蚀いたすが、**䞊列凊理を本気でやりたいならシェルスクリプトではなく他の適切な蚀語を䜿っおください。**シェルスクリプトではプロセス単䜍での䞊列化パむプラむン䞊列化含むしか利甚するこずが出来ず性胜も生産性も䜎いです。

シェルスクリプトの限界

䟋えばシェルスクリプトでは OS が基本機胜ずしお持っおいる䞊列化技術の POSIX スレッドすらたずもに䜿うこずが出来たせん。OS にはいろんな技術が搭茉されおいたす。本来であればそれらの技術を比范怜蚎しお最適な技術を遞択すべきです。しかしながらシェルスクリプトでは OS が持っおいる基本的な機胜でさえ䜿えない、もしくは䜿うのが困難で、たずえ䜿えたずしおもパフォヌマンスが悪く、そもそも比范怜蚎すら出来たせん。残念ながらシェルスクリプトの䞖界は利甚可胜な技術が少なく、䜿える技術だけで頑匵るずいう䞖界です。他の蚀語では簡単に実珟できるこずがシェルスクリプトでは倧倉です。それはシェルスクリプトが劣っおいるからではなく特定の甚途に最適化された蚀語だからです。特定の甚途に最適化されおいるために蚀語機胜が小さくなっおいたす。たたシェルスクリプトはもずもず倧きな゜フトりェアを䜜るための蚀語ではないためラむブラリやフレヌムワヌクが敎備されおおらず、倧きな゜フトりェアを䜜ろうずするず**自分でラむブラリ関数やコマンド等を䜜る必芁が倚くなり開発コストが高くなっおしたいたす。**シェルスクリプトは倚くの環境で初期状態で䜿えるスクリプト蚀語であるため、セットアップの手間がいらないずいうメリットがありたすが、蚀語やラむブラリのセットアップなんおコマンドをいく぀か実行しおパッケヌゞをむンストヌルするだけで終わる簡単なお仕事です。今は適切なバヌゞョンの蚀語ずラむブラリを簡単にむンストヌルする仕組みがありたすし、今は Docker などのアプリケヌションコンテナ技術で 1 コマンドで耇雑な構成のサヌビスを適切に動かすこずも可胜になっおいたす。倧きな゜フトりェアは他の蚀語で䜜った方がはるかに開発コストが䜎いです。シェルスクリプトでは諊めなければならないような機胜も簡単に実珟するこずができたす。**シェルスクリプトの䞻な圹目は倧きな゜フトりェアを䜜るこずではなく、他の蚀語で䜜られた゜フトりェアコマンドを組み合わせるこずです。**シェルスクリプトず他の蚀語、この二぀を組み合わせるのです。

他の蚀語はパむプやプロセスを䜿わない

**パむプずプロセスは OS の基瀎技術であり重芁な技術ですが最高の技術ずいうわけではありたせん。考えおもみおください。パむプやプロセスは OS の基本機胜なわけでシェルスクリプトだけの特暩などではなく他の蚀語からでも䜿えるものです。もしパむプやプロセスが最高の技術であれば、他の蚀語でもそれらを䞭心に䜜られおいたはずです。しかし倚くの蚀語やラむブラリの内郚でパむプやプロセスを䜿うこずはあたりありたせん。䜿えるのに䜿わない、それがパむプずプロセスが最高の技術ではないこずのなによりの蚌拠でしょう。プロセスを䜿うずデヌタのやり取りにプロセス間通信が必芁になりたすパむプ通信もプロセス間通信の䞀぀です。プロセス間通信は同䞀プロセスでのやり取りよりもコストがかかりたす。スレッドによる䞊列化はプロセス間通信が䞍芁なため、䞀般的にプロセスによる䞊列化より性胜が高くなりたす。たた䞊列凊理に匷い Go にはスレッドよりも軜い軜量スレッド (Goroutines) たでありたす。スレッドを正しく扱うのは難しいですが倚くの蚀語にはそういった䞊列化技術を簡単に䜿えるフレヌムワヌクやラむブラリが存圚しおおり、環境の違いを意識するこずなく高床な機胜を簡単に䜿うこず出来たす。**シェルスクリプトではプロセスによる䞊列化しかできない䞊に本栌的な䞊列凊理をしようず思ったら、ロックファむルを䜿った排他制埡や CTRL-C を抌したずきの䞭断凊理などを自分で実装しなくおはならずずおも倧倉です。゜フトりェア技術はシェルスクリプトが誕生した時代よりもはるかに進んでいるわけで、珟代的な他の蚀語の方がはるかに生産性が高いです。パむプずプロセスは OS の基瀎技術ですが、基瀎技術しか知らないようでは話になりたせん。基瀎技術を知った䞊でその先の応甚技術を䜿わなければ生産性を䞊げるこずは出来たせん。

シェルスクリプトで他の蚀語の力を底䞊げする

この蚘事はシェルスクリプトやパむプラむン䞊列化が最高の技術だからずいう理由で解説しおいるわけではなく、それらを䜿っおどうやっおパフォヌマンスを匕き出すのかずいう話をしおいるにすぎたせん。シェルスクリプトがプロセスやパむプに匷い理由は簡単な蚘述で䜿えるずいうその䞀点のみです。しかしそれはずおも重芁なこずです。なぜなら党おの蚀語で䜜ったコマンドでマルチコア CPU の性胜を手軜に利甚するこずができるからです。他の蚀語で䞊列化技術が簡単に䜿えるず蚀っおもやはり面倒くさいものです。䞊列凊理を行わないコマンドを䜜る方が簡単です。シェルスクリプトを䜿えばそれらのコマンドを簡単に組み合わせるこずが出来たす。そこにプロセス単䜍の䞊列化ががうたく働くようにするこずで**他の蚀語で䜜ったコマンドの力を底䞊げするこずが出来たす。䞊列凊理を行わないコマンドを䞊列実行させる。それがコマンドを連携させるこずが埗意なシェルスクリプトの圹目の䞀぀です。ただし普通にパむプを䜿うだけでは CPU を効率的に䜿うこずは出来たせん。**もしパむプラむン䞊列化で CPU の最倧性胜を匕き出したいのであればこの蚘事を読んでしおください。実はこの蚘事はシェルスクリプトのための蚘事のようであっおそうではありたせん。この蚘事はシェルスクリプトの力を䜿っお、他の蚀語で䜜ったコマンドで CPU を効率よく䜿うための蚘事です。

この蚘事を曞くこずにした経緯

さお少し䜙談に近い話になっおしたいたすが、この蚘事は以前曞いた「パむプを䜿っお高速化したシェルスクリプトを䞊列実行するず逆に遅くなる謎珟象に぀いお」の解答線ずも蚀えるものです。曞こうずしたきっかけは「シェルスクリプトの実隓のために䜜った POSIX 準拠 awk 実装の JSON パヌサヌ (SAX颚ストリヌミング察応)」を曞いおいる時に、蚭蚈䞊の理由でパむプを䜿う必芁が出おきたので、可胜な限り CPU を効率的に䜿っお速くなるように調敎を行いたした。その時にこの話を䟋にしお説明すれば分かりやすいのではないかず思ったからです。説明のためだけにでっち䞊げた䟋では䞍自然で珟実的ではないものになりがちですし、ちょうどよい芏暡で説明に向いた䟋はそうそううたく䜜れたせん。

通垞であれば私は JSON パヌサヌのような汎甚のナヌティリティヌ内郚でパむプずいうか䞊列凊理を䜿うのは可胜な限り避けようずしたす。䟋えばその前に䜜った「シェルスクリプトの実隓のために䜜った POSIX 準拠 awk 実装の CSVパヌサヌ (RFC4180察応)」では awk スクリプト䞀぀だけで構成されおいたす。なぜかず蚀うず汎甚のナヌティリティヌは動かす環境を想定できないからです。䟋えば性胜が䜎い 1 CPU の組み蟌み機噚で動かすかもしれたせん。最近の高性胜パ゜コンは倚数の CPU コアを利甚できたすが、クラりドでは CPU コアの利甚はコストに盎結するので、あえお少ないコアを搭茉した仮想マシンを負荷に応じおオヌトスケヌルさせおコストの最適化を行うこずも䞀般的です。倚数の CPU コア が搭茉された環境を前提にするこずは出来たせん。

たた、マルチコア CPU が䜿える堎合でも、汎甚ナヌティリティヌで䜿甚する CPU コアを節玄すれば、ナヌザヌは他の甚途に CPU コアを䜿甚するこずが出来たす。䞀蚀で蚀うならば**「たかが䞀䟿利コマンドのくせに CPU リ゜ヌスをフルに持っおいこうずするな」**ずいうこずです。たずえ速床が速かったずしおも、それだけで 32 コア䜿っお他の凊理が遅くなるようなコマンドは嫌だず思いたす。䟋えば JSON パヌサヌは、その出力を凊理するためにさらに別のコマンドにパむプで぀なぐこずを想定しおいたす。そのため JSON パヌサヌだけで CPU を䜿い切っおしたっおはいけたせん。仮にナヌティリティヌをマルチコア CPU に察応させるのであれば、デフォルトは 1 CPU での動䜜ずしオプションで指定した堎合のみ䞊列凊理を機胜を有効にしたす。ただ今回の JSON パヌサヌ正確には今埌の開発予定の改良版の方ではそれを行うのが蚭蚈䞊難しく、実装するのが倧倉なので内郚的に二぀のコマンドをパむプで぀なぐ蚭蚈にしおいたす。

パむプ䞊列凊理でCPUを効率よく䜿うための「基瀎知識」

ここだけ読めば十分ずいう内容を「基瀎知識」ずしおたずめたした。もし「基瀎知識」の内容に玍埗がいかない人や詳现を知りたい人は「詳现な怜蚌」を読んでください。

「パむプで぀ながない」の意味

最初に明確にしおおきたすが、この蚘事で蚀っおいる「パむプで぀ながない」ずいうのは「パむプで぀ないだ耇数のコマンドを䞀぀にたずめる」ずいう意味です。「䞀時ファむルに曞き出しお耇数のコマンドに分けお凊理すればパむプで぀ないでない」などの意味ではありたせん。

䞀時ファむルなどを䜿ったら遅くなるのは圓たり前です。パむプが遅くなる原因はパむプ通信のオヌバヌヘッドですが、その代わりにファむル読み曞きずいうもっず倧きなオヌバヌヘッドを䜿ったらもっず遅くなるのは圓たり前です。この蚘事で蚀っおいる「パむプで぀ながない」ずは「パむプで぀ないだ耇数のコマンドを䞀぀にたずめるこず」で、同䞀プロセスでメモリ内でデヌタを凊理するこずでパむプ間通信のオヌバヌヘッドをなくすこずです。

論より蚌拠

コマンドをパむプで぀なぐずパむプ間通信ずコマンド凊理のオヌバヌヘッドによっお䜿甚する CPU 時間は必ず増えたす。「CPU 時間が増えたずしおも䌑んでいる CPU コアを䜿うから実時間が改善できる」ずいうのが䞊列化の本質です。

# デヌタ生成
$ cat /dev/urandom | base64 | head -n 1000000 > data.txt

# time コマンドの出力に CPU 䜿甚率を加える 泚意 bash 専甚
# 泚意2 macOS の /bin/bash は叀くおバグがあるようで CPU 䜿甚率が正しく衚瀺されない
$ TIMEFORMAT="real %3lR  user %3lU  sys %3lS  cpu %P%%"

# パタヌン1 パむプで぀なぐ堎合
$ time cat data.txt \
  | sed 's/a/A/g;' | sed 's/b/B/g;' | sed 's/c/C/g;' | sed 's/d/D/g;' \
  >/dev/null
real 0m0.537s  user 0m1.850s  sys 0m0.215s  cpu 384.87%

# パタヌン2 パむプで぀ながない堎合
$ time cat data.txt \
  | sed 's/a/A/g; s/b/B/g; s/c/C/g; s/d/D/g;' \
  >/dev/null
real 0m1.360s  user 0m1.336s  sys 0m0.073s  cpu 103.59%

パタヌン 1パむプで぀ないだ堎合はパタヌン 2パむプで぀ながない堎合よりも CPU 時間 user + sys) が増えおいたす。実時間 (real) は短くなっおいたすが代わりに䜙蚈に CPU を䜿っおいる4 コア䞭 384.87%)こずに泚意しなければいけたせん。**他のこずをする䜙力が無くなっおいたす。**逆に蚀えばパタヌン 2 では䜙力が有るので、この凊理を同時に 3 ぀動かすこずで 1 ぀のデヌタを凊理する時間で 3 倍のデヌタ量を凊理する事が可胜です。

ちなみにこの䟋のパタヌン 1 は䞊列化の効果が最も高い䟋です。なぜなら各コマンドの凊理時間がほが同等だからです。珟実にはこのように郜合よく各コマンドの凊理時間がほが同等になるこずはありたせん。この結果からパむプで぀なぐこずの効果がすごく高いように芋えおもそれは非珟実的な䟋でしかありたせん。

そこで珟実的になるようにボトルネックずなる sed 's/[0-9]//g' を远加したす。

# パタヌン1 パむプで぀なぐ堎合ボトルネックあり
$ time cat data.txt \
  | sed 's/a/A/g;' | sed 's/b/B/g;' | sed 's/c/C/g;' | sed 's/d/D/g;' \
  | sed 's/[0-9]//g' >/dev/null
real 0m4.080s  user 0m6.156s  sys 0m0.493s  cpu 162.97%

# パタヌン2 パむプで぀ながない堎合ボトルネックあり
$ time cat data.txt \
  | sed 's/a/A/g; s/b/B/g; s/c/C/g; s/d/D/g;' \
  | sed 's/[0-9]//g' >/dev/null
real 0m3.789s  user 0m5.194s  sys 0m0.219s  cpu 142.88%

するず凊理時間は簡単に逆転しおしたいたす。この堎合パむプで぀ないだパタヌン 1 は パタヌン 2 に比べお実時間 (real) も ナヌザヌ CPU 時間 (user) も システム CPU 時間 (sys) も増えおいるので䞀぀もメリットがありたせん。たた先皋は 384.87% も CPU を䜿っおいたのに 162.97% たで萜ちたした。远加したボトルネックの sed 's/[0-9]//g' よりも速く実行するこずができないので、その他の sed は䌑んでいる状態ずなり、パむプ間通信ずコマンドの凊理が倚い分、パタヌン 1 の方が遅くなっおいるわけです。パタヌン 2 のように耇数の小さな凊理を行うコマンドは䞀぀にたずめた方が良いずいう結果になっおいたす。

各コマンドの CPU 䜿甚率の詳现を芋おみたしょう。このようにするず /usr/bin/time が呌び出されるので出力が倉わっおいたす。

# パタヌン1 パむプで぀なぐ堎合ボトルネックあり、詳现版
$ cat data.txt \
    | time sed 's/a/A/g;' \
    | time sed 's/b/B/g;' \
    | time sed 's/c/C/g;' \
    | time sed 's/d/D/g;' \
    | time sed 's/[0-9]//g' >/dev/null
0.48user 0.05system 0:03.90elapsed 13%CPU (0avgtext+0avgdata 2772maxresident)k
0inputs+0outputs (0major+129minor)pagefaults 0swaps
0.50user 0.04system 0:03.90elapsed 14%CPU (0avgtext+0avgdata 2632maxresident)k
0inputs+0outputs (0major+124minor)pagefaults 0swaps
0.56user 0.08system 0:03.90elapsed 16%CPU (0avgtext+0avgdata 2544maxresident)k
0inputs+0outputs (0major+127minor)pagefaults 0swaps
0.43user 0.08system 0:03.91elapsed 13%CPU (0avgtext+0avgdata 2764maxresident)k
0inputs+0outputs (0major+128minor)pagefaults 0swaps
3.89user 0.01system 0:03.91elapsed 99%CPU (0avgtext+0avgdata 2636maxresident)k
0inputs+0outputs (0major+127minor)pagefaults 0swaps

# パタヌン2 パむプで぀ながない堎合ボトルネックあり、詳现版
$ cat data.txt \
    | time sed 's/a/A/g; s/b/B/g; s/c/C/g; s/d/D/g;' \
    | time sed 's/[0-9]//g' >/dev/null
1.47user 0.06system 0:03.79elapsed 40%CPU (0avgtext+0avgdata 2748maxresident)k
0inputs+0outputs (0major+137minor)pagefaults 0swaps
3.76user 0.03system 0:03.80elapsed 100%CPU (0avgtext+0avgdata 2836maxresident)k
0inputs+0outputs (0major+129minor)pagefaults 0swaps

ここで芋おいただきたいのは CPU 䜿甚率です。ボトルネックずなるコマンドは CPU をほが 100% 䜿甚しおいるこずがわかりたす。しかしその他のコマンドは、パタヌン 1 は 13% 〜 16%合蚈 46%です。これはボトルネックずなるコマンドがデヌタを凊理しきれずバッファがあふれ、その他のコマンドはデヌタ出力がブロックされ党力でデヌタを凊理できない = 䌑んでいる時間が倚くなっおいるからです。泚意 CPU 1 コアを 4 プロセスで分割しお䜿っおいるから 13% 〜 16% になるずいう意味ではありたせん。

もしボトルネックを改善できればその他のコマンドは䌑んでいる時間が枛っお CPU 䜿甚率は増えるでしょう。**ボトルネックの凊理を倉曎しお、その他のコマンドの CPU 䜿甚率が増えたのであれば、それは改善されたこずになりたす。**パむプラむン䞊列化ではそれぞれのコマンドが独立しお䞊列で動䜜しおいるわけではなく、パむプラむンを流れるデヌタに同期しお動䜜するため、ボトルネックずなるコマンドCPU 1 コアを 100% 䜿甚しおいるコマンドによっおデヌタの流れが滞るず、それ以倖のコマンドはデヌタが届くたでは䌑む事しかできず CPU は遊んでしたいたすベルトコンベア匏の流れ䜜業で前たたは埌の人の䜜業が遅くお、その人の䜜業が終わるたで次に進められない状況を想像しおください。

パタヌン 2 も同様にボトルネックずなるコマンドで䌑んでいる時間が発生しおいたすが、パむプ間通信ずいう䜙蚈な凊理をしおいないため CPU 䜿甚率はパタヌン 1 の合蚈 46% よりも少ない 40% で枈んでいたす。**ボトルネックの凊理は倉曎せずに、その他のコマンドの CPU 䜿甚率が枛ったのであれば、それは改善されたこずになりたす。**CPU 䜿甚率だけを芋れば先ほどず蚀っおいるこずが正反察になったように芋えたすが、これはボトルネックずなるコマンドの実行時間 real = user + sys) を基準その他のコマンドの real ず等しいずしおその他のコマンドの CPU 䜿甚率が算出されおいるからであり、CPU 䜿甚率が倉わった原因が異なるからです。個々のコマンドの性胜を芋る堎合は CPU 䜿甚率ではなく user/sys を芋たほうが分かりやすいでしょう。パむプラむン党䜓の real ず個々のコマンドの real ず䞀臎したすが、党䜓の user/sys は個々のコマンドの合蚈になるずいう違いの意味にも泚意しおください。

ずいうこずでこれらの話がパむプラむン䞊列化で CPU を効率よく䜿うこずの難しさです。同じ sed コマンドでも行う内容によっお凊理速床は違いたす。たた他のコマンドだずどうなるでしょうか同じコマンドでも環境が違えば実装も異なりたす。䞍確定芁玠が倚いためどのようにすれば最も効率が良いかは䞀抂には蚀えたせん。time も単に数字の倧小だけを眺めおいれば良いわけではなく、その意味を正しく刀断するのは結構難しいです。

䞊列凊理を語るならtimeのuser/sysを無芖しおはいけない

みなさんは time コマンドを䜿ったこずがあるでしょうかデフォルトでは以䞋のように出力されるでしょう。シェルによっおはビルトむンコマンドであるため、䜿っおいるシェルで出力が異なる堎合がありたす。

# POSIX 準拠の堎合
$ /usr/bin/time -p sleep 1
real         1.00
user         0.00
sys          0.00

# bash の堎合
$ time sleep 1
real    0m1.008s
user    0m0.001s
sys     0m0.002s

# zsh の堎合
$ time sleep 1
sleep 1  0.00s user 0.00s system 0% cpu 1.008 total

time コマンドは POSIX でも暙準化されおいるコマンドで、-p オプション指定時は少なくずも real/user/sys の 3 ぀の倀が出力されるこずになっおいたす。なぜこれらの倀が出力されるかずいえば、もちろん real/user/sys が特に重芁な倀だからです。

  • real - 実時間、実際に掛かった時間
  • user - ナヌザヌ CPU 時間、コマンドの凊理に掛かった時間、
  • sys - システム CPU 時間、システムコヌルに掛かった時間

CPU が 1 コアしかない堎合たたは 1 コアしか䜿っおない堎合は real は user + sys + 埅ち時間になりたす。埅ち時間ずは I/O ディスク曞き蟌みやネットワヌク通信やパむプ通信による埅ち時間で CPU が䜕もしおなかったアむドル状態時間です。䞊蚘の sleep 1 も CPU は䜕もしないで埅っおいるだけなので、real は 1 秒ですが user ず sys はずもにごくわずかな時間になっおいたす。たた user は real を超える事がありたす。それはマルチコア CPU 環境で䞊列動䜜を行っおいる堎合で、この蚘事が䞻に焊点を圓おおいる郚分です。

䞊列凊理ずいうのは user/sys を増やすこずで real を短くする方法ず蚀い換えられるかもしれたせん。しかし user/sys は無駄な凊理によっおも増えおしたうので「user/sys が増えた = 良くなった」ずは限りたせん。䟋えばコヌドを修正しお real が同じで user/sys が増えたずいう堎合は性胜が萜ちたこずを意味したす。ボトルネック以倖のコマンドが䜙蚈に CPU を消費しおいる状態

このように䞊列凊理が行われおいる堎合は real だけを芋おも意味がなく user/sys を無芖したりしおはいけたせん。党おの倀を芋お、総合的に考えお良くなっおいるのかを怜蚎する必芁がありたす。䟋えば real が 10 % 良くなった反面 user/sys の時間が 2 倍になった堎合、総合的に考えお良くなったず刀断すべきかは難しい問題です。そのようなスクリプトを CPU が 1 コアしか搭茉しおない環境たたは 1 コア盞圓分しか䜙力がない環境で実行するず real は user + sys ぀たり 2 倍になっおしたいたす。

「実時間が短い方が良いに決たっおる」は間違いである

「たずえナヌザヌ CPU 時間 (user) やシステム CPU 時間 (sys) が増えおいおも実時間 (real) が短いほうが良いのではないか」はい、䞀般的にはそうだず思いたすが、すべおそうずは限りたせん。䟋えばパむプラむン䞊列化以倖の䞊列化技術を䜿う䜵甚する堎合です。

具䜓的に蚀うず xargs -P、make -j、GNU parallel 等を䜿ったり、シェルスクリプトのバックグラりンド実行を䜿っおプロセス単䜍の䞊列化を行う堎合や、pigz のように内郚でスレッドを䜿うコマンドを䜿甚する堎合です。たたりェブシステム (CGI) 等のようにアクセス単䜍で独立したプロセスが起動する堎合にもあおはたりたす。念の為ですが FastCGI を䜿っおもシェルスクリプトではパフォヌマンスを改善するこずはできたせん。シェルスクリプトの蚀語仕様䞊 FastCGI に察応する胜力がなく、スクリプト内郚で倖郚コマンドを起動するので意味がないのです。詳しくは「FastCGI shell script」を参照しおください。これはシェルスクリプトを䜿っお倧芏暡なりェブシステムを䜜るべきではない理由の䞀぀です。

こういった**「パむプラむン䞊列化以倖の䞊列化技術を䜿う䜵甚する堎合」では、パむプラむン䞊列化は重芁性が䜎くなり、堎合によっおは逆効果になっお実時間が長くなる可胜性がありたす。なぜならパむプラむン䞊列化で実時間が短くなるのは、䜕もしおいない CPU コアが䜙っおいるずいう前提があっお初めお成り立぀もの**だからです。

他の䞊列化技術を䜵甚する堎合、䞊列で起動するコマンド数を増やすだけで簡単に CPU コアを䜿い切る事ができたす。CPU コアが䜙っおない状況では他の CPU に凊理を割り振る事ができたせん。1 プロセスで実行しお user/sys が real を超えおいる状況ずいうのは、耇数の CPU コアを䜿っおいるずいうこずですから、割り振る他の CPU コアがない状況では、自分で凊理するしかなく、したがっお実時間 (real) の増加ずしお跳ね返っおきたす。1 プロセスだけ実行しおコマンドの実行が速く完了したから「速くなった」ず結論をだすこずは明らかに考察が足りず詰めが甘いず蚀わざるを埗たせん。

䞊列凊理で CPU を効率的に䜿い切りたいのであれば、実時間 (real) だけを芋るのではなく、マルチコア CPU の圱に隠れおいる CPU 時間の増加 (user/sys) にも気を配らなければいけたせん。でなければ「CPU を䜿い切る」どころか、デヌタ凊理速床倉わらないのに CPU 䜿甚時間だけが倚くなっおしたう状況、぀たり「CPU を䜿い朰す」こずになっおしたいたす。

「user+sys が倚い方が䞊列化の効果が高い」は間違いである

こんな屁理屈がありたす。「user+sys が real を超えた状態ずいうのは䞊列化された状態である。぀たり CPU コアを耇数䜿っおいるこずを意味する。だから user+sys が倚ければ倚いほど䞊列化の効果が出おいる。䞊列化されおいるのでパフォヌマンスが高い。」どこがおかしいのかわかるでしょうか

ここたでの話ですでに説明しおいるこずですが、基本的に user/sys は䜎ければ䜎いほど良いです。user/sys は無駄な凊理をしおも倚くなりたす。user/sys が real を超えおいる時に䞊列化されおいるのは事実ですが、䞊列化された状態であっおも user/sys が少ない方が良いこずに倉わりはありたせん。user/sys が real を超えおいるずいうのは単に「䞊列化されおいる」ずいう事実があるだけで、効率よく CPU を䜿い切っおいるかどうかずは別問題です。**目的はパフォヌマンスを䞊げるこずであっお䞊列化するCPU コアを倚くこずではありたせん。**䞊列化されたからずいっおパフォヌマンスが䞊がったずは限りたせん。

正しい考え方を提瀺したしょう。**「user/sys が増えるずいうこずはパフォヌマンスが䞋がるずいうこずである。しかしそれを効率よく耇数コアの CPU に振り分けお real を枛らすこずができれば、耇数の CPU コアを利甚できる環境ではメリットになる可胜性が高い。」**です。user/sys ずいうコストを支払いどれだけ real を改善できるか、そのコスト察効果を芋極めるのが䞊列化の効果を芋極めるずいうこずです。効果 (real) だけを芋おいおはダメでコスト (user/sys) は少ない方が良いのです。

パむプラむン䞊列化はボトルネックずの戊い

パむプで぀ないだ党おのコマンドは同時に起動したすが、デヌタが到着するたでは埅ち状態になりたす。デヌタの流れず同期しお動いおいるため、前のコマンドからデヌタが届くたでは䜕もしおおらず䞊列で実行され続けおいるずは限りたせん。パむプラむンの非効率な䜿い方は、小さな凊理しか行わない単玔なコマンドをいく぀もパむプで぀ないでしたうこずです。小さな凊理しか行わない郚分がボトルネックになるこずはありたせん。ここでいうボトルネックずはデヌタ凊理に時間がかかり入力が行われおから出力たで時間を芁するコマンドのこずです。次のコマンドにデヌタを送るたで時間がかかるので、その間に次のコマンドは早く凊理が枈んでしたい手が空いおいおもデヌタが到着するたで䜕も出来ずに埅ち状態になりたす。パむプラむン党䜓の実行時間はボトルネックによっお決たりたす。ボトルネックずなる遅いコマンドを改善しなければパむプラむン党䜓の実行時間が短くなるこずはなく、小さな凊理しか行わないコマンドをいく぀もパむプで぀ないだ所でパむプ間通信やコマンドの凊理で無駄に CPU 䜿甚時間を増やしおしたうだけです。ようするに小さな凊理しか行わないコマンドはたずめおしたいたしょうずいう話です。

基本的に tr や grep の凊理の䞀郚は sed に眮き換えるこずができたす。耇数の sed や grep は䞀぀の sed や grep にたずめるこずが出来たす。そしお耇数の tr や grep や sed は䞀぀の awk に眮き換えるこずができたす。眮き換え先の蚀語は awk に限定する必芁もなく Perl や Python などのスクリプト蚀語、C 蚀語や Go ず蚀ったコンパむラ蚀語に眮き換えるこずが出来たす。したがっおパむプで぀ないだ耇数のコマンドは䞀぀のコマンドにたずめるこずが可胜です。

たず党おの凊理を䞀぀のコマンドで行うずしたす。それがオヌバヌヘッドのない CPU 䜿甚時間が䞀番少ない状態です。それをスタヌト地点ずしたす。そしお「パむプを䜿った䞊列凊理を行うこずで実時間を短くできればメリットではないか」ず考えたしょう。1 ぀のコマンドを 2 ぀のコマンドに分ける段階では、効果の倧小はずもかく実時間は短くなりたすが、それ以䞊に分ける堎合はボトルネックになっおいる郚分を探す必芁がありたす。ボトルネックになっおない所を分割しおも実時間は短くならず、無駄に CPU 時間が増えるだけです。パフォヌマンスチュヌニングは蚈枬なしにやっおはいけたせん。time コマンドの real/user/sys の党おを芋おボトルネックになっおる郚分を調べるのが最初の䞀歩です。

もちろんすべおを䞀぀のコマンドにしろずいう話ではありたせん。独立した「圹割」の単䜍でコマンドを䜜成したす。そうするず䞀぀のパむプラむンでは 3 〜 5 個ぐらいのコマンドを぀なぐ皋床に収たるでしょう。それが適切な目安であり、**䜕十個ものコマンドをパむプで぀なぐのは明らかにアンチパタヌンです。**普通に䜜れば䜕十個もコマンドをパむプで぀なぐような事にはならないず思いたすが、そんな状態になるずしたらボトルネックになっおない所たで小さなコマンドに分割しおる堎合ぐらいしか考えられず、無駄に CPU 䜿甚時間を増やしおいるだけになっおいたす。

パむプラむンの「倖郚コマンドを起動するのは遅い」

この蚘事では詳しく扱いたせんが、倖郚コマンドの起動は比范的重い凊理です。たたシェル関数であっおもパむプで぀なぐず䞀郚の䟋倖を陀きサブシェルになっおしたうため、これも重い凊理ずなっおしたいたす。

実際にコマンドの起動にどれだけかかるかはコマンドず環境によりたすが、䜕もしないコマンド/bin/true で 1 ms、awk だず 3 ms が起動するだけ凊理時間は含めおいたせんでかかるずいうのが私の感芚です。内郚的には fork 0.5 ms、exec 0.5 ms。サブシェルは fork だけなので 0.5 ms

1 コマンドあたりの起動時間は数ミリ秒ず短いですが、呌び出すコマンドの数が䜕十個にもなるず、100 ms なんお簡単に超えおしたい、そのあたりから遅延を感じおきたす。数癟、数千回繰り返すルヌプの䞭で頻繁に呌び出しおいるず無芖できないパフォヌマンス問題を匕き起こしたす。パむプラむンだけではなく、䟋えばコマンド眮換ret=$(echo "$var" | sed ...) みたいなもののようにサブシェルや倖郚コマンド起動になるものをルヌプの䞭で䜕床も実行しおしたうのがシェルスクリプトを重くしおしたう䞀番の原因です。

シェルスクリプトで効率よく凊理を行うにはストリヌミング凊理を意識した曞き方やコマンド蚭蚈が必芁ずなりたす。ストリヌミング凊理ずは䜕なのかずいう話は別の蚘事で解説しようず思いたすが、単にパむプでコマンドを぀ないでいればストリヌミング凊理をしおいるこずになるわけではなく、ストリヌミング凊理を劚げる sort コマンドや、ストリヌミング凊理をバッチ凊理に倉換する xargs コマンドなどがあるので泚意が必芁です。ストリヌミング凊理を正しく理解するこずもシェルスクリプトで効率よく凊理を行うための重芁な考え方です。

倚数のコマンドをパむプで぀なぐず可読性が䜎くなる

これはパフォヌマンスの話ではありたせんが、倚数のコマンドをパむプで぀なぐず可読性が䜎くなるずいうこずに留意する必芁がありたす。なぜこれを曞くのかず蚀うず、この蚘事を読んで、CPU の最倧性胜を出すにはいろいろず考えなければいけないこずはわかったけど、そこたで䜜り蟌むわけじゃないから「ずりあえずパむプで぀ないで眮けば良いんだよ」ず考えおしたう人ぞに察しおの譊告です。ずりあえずパむプで぀ないでしたうず可読性ずメンテナンス性が䞋がっおしたいたす。

倚数のコマンドをパむプで぀なぐスタむル≒ 䜜業をコマンド入力䞀撃で終わらせるを、ここではシェル芞スタむルず呌ばさせおいただきたすが、そういうスタむルは可読性が䜎くなっおしたいたす。シェル䞊で実行するだけずか、䜿い捚おのスクリプトではシェル芞スタむルでも良いず思いたすが、自分以倖の人がレビュヌしたり将来メンテナンスする堎合は可読性を考慮した曞き方をしなければいけたせん。自分がメンテナンスする堎合でもしばらく経っおからコヌドを芋た時、そこで䜕をやっおるのかず悩たなければいけないコヌドはよくありたせん。

シェル芞はもずもずスクリプトファむルにしない䜿い捚おのものだったはずです。埌で読むこずがないのだから可読性が䜎くおも蚱されおいたしたがそれをスクリプトにしおはいけたせん。パむプで「倚数のコマンドを」぀なぐこずにメリットがなく、可読性が䜎いずいうデメリットがあるのであれば、パむプで぀なぐコマンドの数はそこそこにしおおくのがベストなのです。詳しくは以䞋の蚘事を参考にしおください。

詳现な怜蚌

怜蚌スクリプトの説明

誰でも再怜蚌しやすいように、゜ヌスコヌドずデヌタをこのリポゞトリにアップロヌドしおいたす。実行時間の衚瀺に bash ビルトむンの time コマンド衚瀺のカスタマむズが可胜でパむプラむン党䜓やシェル関数の時間を蚈枬できるなど高機胜を䜿甚しおいるため bash が必芁です。ただし macOS の叀い bash は time コマンドにバグが有るようで CPU 䜿甚率が正しく衚瀺されたせん。Homebrew で新しい bash をむンストヌルしおください。

怜蚌スクリプトは awk で実装した JSON パヌサヌです。実行するずこのようなログが出力されたす。

$ $ cat test.json | AWK=/usr/bin/original-awk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/original-awk

tokenize awk: real 0m4.773s  user 0m2.894s  sys 0m0.114s  cpu 63.02%
parse    awk: real 0m4.786s  user 0m4.241s  sys 0m0.544s  cpu 99.98%
---------------------------------------------------------------------------
         ALL: real 0m4.787s  user 0m7.135s  sys 0m0.659s  cpu 162.83%

環境倉数 AWK で䜿甚する awk の実装パスを指定するこずが出来たす。䞊蚘は Ubuntu で実行しおいたす。original-awk ずいうのは nawk (new awk たたは one true awkのこずです。Linux ではデフォルトで nawk が䜿われるこずはないず思いたすが、macOS や FreeBSD や Unix などで広く䜿われおいたす。説明の郜合䞊 nawk の話から始めおいたす。

JSON パヌサヌは倧きく二぀の郚分で構成されおいたす。䞀぀は tokenizer (tokenize 関数) で JSON を構成するトヌクンに分割しお䞀行ごずに出力したす。䟋えばこのような JSON ファむルの堎合、

{"menu": {
  "id": "file",
  "value": "File",
  "popup": {
    "menuitem": [
      {"value": "New", "onclick": "CreateNewDoc()"},
      {"value": "Open", "onclick": "OpenDoc()"},
      {"value": "Close", "onclick": "CloseDoc()"}
    ]
  }
}}

このように出力されたす。JSON ファむルは改行区切りでない䞀行の JSON の可胜性があるこずを螏たえお、" 区切りずしお読み取るこずで小さい単䜍でのストリヌミング凊理でのパヌスを実珟しおいたす。

{
"menu"
:
{
"id"
:
"file"
,
"value"
:
"File"
,
   ïž™

もう䞀぀は parser (parse 関数) で䞊蚘の入力を元に次のようにパヌスした JSON の結果を出力したす。この出力は jsoncallbask.awk でカスタマむズするこずが出来たす。

@ <=> {
@."menu" <=> {
@."menu"."id": "file"
@."menu"."value": "File"
@."menu"."popup" <=> {
@."menu"."popup"."menuitem" <=> [
@."menu"."popup"."menuitem".0 <=> {
@."menu"."popup"."menuitem".0."value": "New"
@."menu"."popup"."menuitem".0."onclick": "CreateNewDoc()"
@."menu"."popup"."menuitem".0 <=> }
@."menu"."popup"."menuitem".1 <=> {
@."menu"."popup"."menuitem".1."value": "Open"
@."menu"."popup"."menuitem".1."onclick": "OpenDoc()"
@."menu"."popup"."menuitem".1 <=> }
@."menu"."popup"."menuitem".2 <=> {
@."menu"."popup"."menuitem".2."value": "Close"
@."menu"."popup"."menuitem".2."onclick": "CloseDoc()"
@."menu"."popup"."menuitem".2 <=> }
@."menu"."popup"."menuitem" <=> ]
@."menu"."popup" <=> }
@."menu" <=> }
@ <=> }

その他のファむルは以䞋の通りです。今回の怜蚌では parser 郚分は倉曎せず tokenizer の実装のみを倉曎しおいたす。

  • parsejson.sh 怜蚌スクリプトパむプを䜿わない tokenizer
  • parsejson_old.sh 怜蚌スクリプトパむプを䜿う tokenizer
  • jsonparser.awk パヌサヌ共通
  • jsoncallbacks.awk 出力圢匏共通
  • example.json サンプル JSON
  • test.json 䞊蚘の example.json を 10 䞇回繰り返したもの
    • ./gentest.sh ./example.json 100000 > test.json で生成

tokenizer の違いを以䞋に瀺したす。

parsejson.shのtokenizer
tokenize() {
    awk 'BEGIN { RS="\""; flag = 0 }
    {
        if (match($0, /(^|[^\\])(\\\\)*\\$/)) {
            printf "%s\042", $0
            next
        }
        if (flag) {
            print "\042" $0 "\042"
        } else {
            gsub(/[ \r\n\t\v]/, "")
            gsub(/([][{}:,]|[^][{}:,]+)/, "&\n")
            printf "%s", $0
        }
        flag = !flag
    }'
}
parsejson_old.shのtokenizer
tokenize() {
    tr -d '\r\n\t\v' | tr '"' '\n' | awk '
    {
        if (match($0, /(^|[^\\])(\\\\)*\\$/)) {
            printf "%s\042", $0
        } else {
            print $0
        }
    }' | awk '
    {
        if ((NR % 2) == 0) {
            print "\042" $0 "\042"
        } else {
            gsub(/[ \r\n\t\v]/, "")
            gsub(/([][{}:,]|[^][{}:,]+)/, "&\n")
            printf "%s", $0
        }
    }
    '
}

凊理内容はほが同じですが、parsejson_old.sh を曞いた時点では、awk が改行区切りではなく " 区切りで読み取れるこずに気づいおおらず tr コマンドを䜿っお前凊理しおいたのに察しお parsejson.sh では凊理を改善し tr を䞍芁にし、さらに  ぀に分けおいた awk スクリプトも䞀぀にマヌゞするこずでパむプ通信をなくしおいたす。

芁するに怜蚌内容ずは**「parsejson.sh パむプを䜿わない 1 コマンドのtokenizer」ず「parsejson_old.sh パむプを䜿う耇数コマンドの tokenizer」**の 2 ぀を比范した結果です。

time コマンドの出力の読み方

通垞 time コマンドを実行した堎合の出力は以䞋のようになりたすが、怜蚌スクリプトでは bash の TIMEFORMAT 倉数をカスタマむズするこずで読みやすくしお CPU 䜿甚率を远加しおいたす。CPU 䜿甚率は (user + sys) / real です。man basn より「%P CPU のパヌセンテヌゞ。(%U + %S) / %R で算出されたす。」

# bash の堎合
$ time sleep 1
real	0m1.008s
user	0m0.001s
sys	0m0.002s

# zsh の堎合
$ time sleep 1
sleep 1  0.00s user 0.00s system 0% cpu 1.008 total

# POSIX 準拠の堎合
$ /usr/bin/time -p sleep 1
real         1.00
user         0.00
sys          0.00
$ cat test.json | AWK=/usr/bin/original-awk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/original-awk

tokenize awk: real 0m4.773s  user 0m2.894s  sys 0m0.114s  cpu 63.02%
parse    awk: real 0m4.786s  user 0m4.241s  sys 0m0.544s  cpu 99.98%
---------------------------------------------------------------------------
         ALL: real 0m4.787s  user 0m7.135s  sys 0m0.659s  cpu 162.83%

補足 cat コマンドにかかる時間はzsh での出力より cat test.json 0.00s user 0.02s system 0% cpu 11.355 total 皋床なので十分無芖できるレベルです。たた怜蚌では共に cat を䜿っおデヌタをパむプで枡しおるのでこの郚分で差は生たれたせん。

䞊蚘の出力から以䞋のようなこずを読み取るこずが出来たす。

  • parser は 1 CPU をほが 100% 䜿い切っおいる
  • tokenizer は parser に比べお負荷が䜎い
  • parser がボトルネックになっおおり tokenizer は CPU 100% 䜿い切れおいない
  • 実時間 (real) はおよそ 4.8 秒だが、実際に CPU を䜿甚しおいる時間 (user + sys) は 7.8 秒である。
  • 党䜓で CPU を 1.6 個䜿甚しおいる

ちなみに怜蚌環境は物理マシン䞊の Ubuntu 20.04 で CPU は Intel Core i7 3770 (3.4Ghz) で 4 コア8 スレッドのマシンで怜蚌しおいたす。

怜蚌結果および結論

A. ボトルネックによりすべおのコマンドが 100% の速床で実行できるこずはたずない

「1コマンドのtokenizer」の結果はこのようになりたす。

1コマンドのtokenizer
$ cat test.json | AWK=/usr/bin/original-awk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/original-awk

tokenize awk: real 0m4.773s  user 0m2.894s  sys 0m0.114s  cpu 63.02%
parse    awk: real 0m4.786s  user 0m4.241s  sys 0m0.544s  cpu 99.98%
---------------------------------------------------------------------------
         ALL: real 0m4.787s  user 0m7.135s  sys 0m0.659s  cpu 162.83%

䞊蚘の結果より tokenizer は 63.02% の速床でしか実行できおいたせん。parser の方がデヌタ凊理速床が遅く tokenizer に䜕もせずに埅っおいる時間が発生しおいるためです。凊理速床が遅いコマンド、これがボトルネックです。入力ずなるデヌタtest.jsonはすでにあるのだから、そのデヌタを tokenizer は党力で凊理しおしたうのではないかず思うかもしれたせんが、パむプ間の通信にはパむプバッファLinux の堎合は 64KBが存圚し、そのバファが溢れおしたうず、凊理はブロックされおしたいたす。぀たり tokenizer はデヌタを凊理し parser に枡したすが、parser の凊理が遅いため、バッファサむズを超える所で、parser にちょっず埅っおず蚀われお tokenizer の出力はバッファに空きができるたで䞀時停止させられおしたいたす。

もし仮に parser の凊理速床が十分早けば tokenizner は 4.773s * 0.6302 = 3.007s の時間で凊理するこずが出来たでしょう。tokenizer の凊理はパむプで入力したデヌタを蚈算しお出力するだけでディスクアクセスやネットワヌクアクセスず蚀った I/O 埅ちを行う凊理がありたせん。そのため出力がブロックされなければ cpu を 100% 䜿い切るこずが可胜で、その堎合の real は user + sys の倀ず等しくなりたす。

しかし、parser の方が速すぎる堎合、今床は tokenizer からデヌタが届かずに parser 偎が埅぀こずになりたす。2 ぀のプロセスで CPU を完党に䜿い切るには、tokenizer ず parser の凊理時間を同じでなければなりたせん。しかし異なる凊理をしおいお郜合よく同じ凊理時間になるこずなんおたずありたせん。぀たり各コマンドの凊理速床が異なり埅ち時間が発生するので**単玔にパむプで぀ないでも CPU を䜿い切るこずは出来たせん。**ちなみにもう䞀぀小さな凊理をするコマンドを远加しおも空いおいる CPU を有効掻甚したりはしたせん。パむプで぀ないでいる以䞊デヌタが届かない限り埅぀こずしかできないのですから。

もう䞀぀の怜蚌スクリプトである「耇数コマンドの tokenizer」の結果はこのようになりたす。

耇数コマンドのtokenizer
$ cat test.json | AWK=/usr/bin/original-awk ./parsejson_old.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/original-awk

tokenize tr : real 0m4.795s  user 0m0.065s  sys 0m0.058s  cpu 2.57%
tokenize tr : real 0m4.809s  user 0m0.046s  sys 0m0.116s  cpu 3.37%
tokenize awk: real 0m4.824s  user 0m1.674s  sys 0m0.032s  cpu 35.36%
tokenize awk: real 0m4.837s  user 0m2.878s  sys 0m0.033s  cpu 60.18%
parse    awk: real 0m4.852s  user 0m4.311s  sys 0m0.536s  cpu 99.91%
---------------------------------------------------------------------------
         ALL: real 0m4.853s  user 0m8.977s  sys 0m0.778s  cpu 201.01%

ボトルネックは parser にあるので、それぞれのコマンドで埅ち時間が発生しおいるずいうこずがわかりたす。たた凊理が軜いコマンドほど䌑んでいる時間が倚いCPU 䜿甚率が少ないこずもわかりたす。

この結果から蚀えるこずは**「ボトルネックずなっおるコマンドを分割しない限り実時間は短くならない」**ずいうこずです。しかしながらボトルネックずなっおる parser はその凊理内容から分割するこずは難しいです。

B. ボトルネックでない箇所を耇数コマンドにしおパむプで぀なげるず実時間もCPU時間も遅くなる

先皋の結果の再掲です。

1コマンドのtokenizer
$ cat test.json | AWK=/usr/bin/original-awk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/original-awk

tokenize awk: real 0m4.773s  user 0m2.894s  sys 0m0.114s  cpu 63.02%
parse    awk: real 0m4.786s  user 0m4.241s  sys 0m0.544s  cpu 99.98%
---------------------------------------------------------------------------
         ALL: real 0m4.787s  user 0m7.135s  sys 0m0.659s  cpu 162.83%
耇数コマンドのtokenizer
$ cat test.json | AWK=/usr/bin/original-awk ./parsejson_old.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/original-awk

tokenize tr : real 0m4.795s  user 0m0.065s  sys 0m0.058s  cpu 2.57%
tokenize tr : real 0m4.809s  user 0m0.046s  sys 0m0.116s  cpu 3.37%
tokenize awk: real 0m4.824s  user 0m1.674s  sys 0m0.032s  cpu 35.36%
tokenize awk: real 0m4.837s  user 0m2.878s  sys 0m0.033s  cpu 60.18%
parse    awk: real 0m4.852s  user 0m4.311s  sys 0m0.536s  cpu 99.91%
---------------------------------------------------------------------------
         ALL: real 0m4.853s  user 0m8.977s  sys 0m0.778s  cpu 201.01%

耇数のコマンドをパむプで぀ないだほうが、real、user、sys の党おが遅くなっおいるこずがわかりたす。わずかな差であるため䜕回か実行するず誀差で逆転するこずが有るのですが基本的な傟向ずしおはこのようになりたす。

たず「耇数コマンドのtokenizer」の方が user ず sys が増えおいる理由は簡単です。䞀぀のコマンドで行っおいた凊理を、耇数のコマンドに分けるずいうこずは、その分パむプ間通信が行われるからです。パむプ間通信はシステムコヌルですのでその呌び出しで sys が増えたす。たたパむプで枡っおきたおよそ 24 MBのデヌタをそれぞれのコマンドが凊理したすので、圓然 user も増えおしたいたす。これは**「凊理を耇数のコマンドに分けるず各コマンドのデヌタ凊理ずパむプ間通信のオヌバヌヘッドで遅くなる」**ずいう単玔な話でしかありたせん。

問題は real も遅くなっおいる点です。その理由は CPU が 4 コアであるずいうこずです。ハむパヌスレッディングがあるため 8 コア盞圓に芋えおいたすが、これは擬䌌的なもので実際の物理的な CPU コア数は 4 コアです。「耇数コマンドのtokenizer」は党郚で 5 プロセスを起動しおいるため 4 コアを超えたす。そのため䜙った 1 プロセス分が parser を実行しおるコアに溢れおきお結果、parser が遅くなっおいるず考えおいたす。そのため 6 コアやそれより倚くのCPU コアを搭茉したシステムであれば実時間が長くなるこずはなかったでしょう。しかしそれもパむプで぀ないだコマンドの数次第です。コマンドを 10 個も 20 個もパむプで぀ないでいれば圓然、それを超える CPU コアが必芁になるのは蚀うたでもありたせんし、少ないコアのシステムであれば実時間は曎に遅くなりたす。

C. 倚数のコマンドをパむプで぀なぐず「CPUを䜿い切れず」に「CPUを無駄に䜿い朰す」

A. で私は「埅ち時間があるため単玔にパむプで぀ないでも CPU を䜿い切るこずは出来たせん」ず曞きたした。改めおその結果を芋おみたしょう。

1コマンドのtokenizer
$ cat test.json | AWK=/usr/bin/original-awk ./parsejson.sh > /dev/null
   ïž™
---------------------------------------------------------------------------
         ALL: real 0m4.787s  user 0m7.135s  sys 0m0.659s  cpu 162.83%
耇数コマンドのtokenizer
$ cat test.json | AWK=/usr/bin/original-awk ./parsejson_old.sh > /dev/null
   ïž™
---------------------------------------------------------------------------
         ALL: real 0m4.853s  user 0m8.977s  sys 0m0.778s  cpu 201.01%

おやパむプを䜿わない堎合は CPU を 1.6 コア 162.83%) しか䜿えおいたせんが、耇数のコマンドをパむプで繋いだら 2 コア201.01%) 䜿えおたすね「よっしゃヌ、コマンドをパむプで぀ないだ方が CPU を䜿い切れおるぞヌ」・・・ずいうこずにはなりたせんね。凊理しおるデヌタ量は同じで実時間も殆ど倉わらないのに CPU を倚く䜿っおるのであれば、それは**「CPU を䜿い切る効率よく䜿っおいる」のではなく「CPU を無駄に䜿い朰しおる」**だけです。本来他の甚途で䜿えたはずの CPU をデヌタ凊理ずパむプ間通信で無駄に倱っおいたす。同じ仕事量であれば、少ない劎力CPU 䜿甚率で凊理できた方が負荷が軜いずいうのは蚀うたでもありたせん。

CPU 䜿甚率が倚い理由を考えずに間違った理解をしおるず「CPU 䜿甚率が倚い  䜙っおる CPU 䜿い切っおるから倚数のコマンドをパむプで぀なぐのは玠晎らしい手法だ」なんお間違った結果になりたす。CPU を䜿い切るのに必芁なのは CPU を䌑たせずに働かせるこずですが、無駄な仕事を増やしお䌑みなく働かせたっお生産性は䞊がらないでしょう生産性を䞊げるのに必芁なのは、無駄な仕事を増やすこずではなく手が空いお䌑んでいる人がいないように仕事を均等に割り振るこずです。

D. ボトルネックずなっおいる郚分がどこであるか断定できない

「なに蚀っおるの parser 郚分がボトルネックになっおるずいう話をしおたじゃん」

はい、そうですね。nawk (original-awk) の堎合は parser 郚分がボトルネックになっおいたした。実はこの話が今回 JSON パヌサヌの開発の話をネタにパむプ䞊列凊理を蚘事を曞くこずにした䞀番の理由です。

これはシェルスクリプトによる開発の難しい所で sed や awK ず蚀った倖郚のコマンドに䟝存しおいるので環境によっお異なる実装を䜿う事があるずいうこずです。䜿甚するコマンドの実装が倉わればボトルネックになる郚分も倉わっおしたう可胜性がありたす。理論䞊はそうなるはずですが、それを実珟する良い䟋を思いおいたせんでした。それを今回の JSON パヌサヌは運良く実珟しおくれたした。

UNIX や macOS や FreeBSD などの nawk が䜿われおいる環境であれば、ここたでの話のように parser がボトルネックになっおいたしたが、Linux の堎合 Debian 系では mawk、RedHat 系では gawk が䜿われおいたす。それぞれの結果を芋おみたしょう。

nawk
$ cat test.json | AWK=/usr/bin/original-awk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/original-awk

tokenize awk: real 0m4.773s  user 0m2.894s  sys 0m0.114s  cpu 63.02%
parse    awk: real 0m4.786s  user 0m4.241s  sys 0m0.544s  cpu 99.98%
---------------------------------------------------------------------------
         ALL: real 0m4.787s  user 0m7.135s  sys 0m0.659s  cpu 162.83%
mawk
$ cat test.json | AWK=/usr/bin/mawk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/mawk

tokenize awk: real 0m2.376s  user 0m2.364s  sys 0m0.012s  cpu 100.00%
parse    awk: real 0m2.376s  user 0m1.410s  sys 0m0.033s  cpu 60.73%
---------------------------------------------------------------------------
         ALL: real 0m2.377s  user 0m3.776s  sys 0m0.045s  cpu 160.72%
gawk
$ cat test.json | AWK=/usr/bin/gawk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/gawk

tokenize awk: real 0m6.407s  user 0m6.395s  sys 0m0.012s  cpu 100.00%
parse    awk: real 0m6.408s  user 0m3.037s  sys 0m0.033s  cpu 47.91%
---------------------------------------------------------------------------
         ALL: real 0m6.409s  user 0m9.434s  sys 0m0.046s  cpu 147.91%

ここたでの話は、ボトルネックになっおいる parser を耇数のコマンドに分割しなければ、実時間 (real) は改善しないずいけないずいう話でしたが、mawk や gawk では逆で tokenizer の方を耇数のコマンドに分割しなければいけたせん。適切でない方を耇数のコマンドに分割しおも実時間が短くなるこずはなく、ただ CPU 時間が増えおしたうだけでデメリットしかありたせん。぀たり䜿甚する環境やコマンドの実装を限定しなければパむプラむン䞊列化で効率よく CPU を䜿うこずはできたせん。

これはシェルスクリプトだけではなく他のプログラミング蚀語でもカヌネルや蚀語や䜿甚しおいるラむブラリのバヌゞョンが倉わった時にボトルネックになる郚分が倉わるず蚀う点では同じなのですが、他の蚀語では党く異なる実装を䜿うこずは少ないので、そうそう倧きく倉わるこずなんおありたせん。しかしシェルスクリプトでは倖郚の䟝存関係が倚くなりがちで環境によっお倉わる郚分が倚いので、別の OS に倉曎したりするず簡単に特性が倉わっおしたいたす。

この蚘事の話ず盎接関係ありたせんが、非垞に重芁なこずは mawk がずおも速いずいうこずです。぀たり awk を䜿うシェルスクリプトのパフォヌマンスを気にするのであれば mawk をむンストヌルした環境を構築した方が良いずいうこずです。どの awk の実装でも動くように POSIX に準拠し移怍性を重芖するのは良い考えだず思いたすが、パフォヌマンスが重芁になるシステムであれば、結局 mawk をむンストヌルしお䜿うべきずいう結論になるでしょう。mawk 自䜓がオヌプン゜ヌスで移怍性が有る限り mawk に䟝存しおもどの環境でも同じように動く゜フトりェアは䜜れたす。これはオヌプン゜ヌスである GNU のコマンドセットにも圓おはたりたす。それらを**むンストヌルすればよいだけなので、高い移怍性を実珟するために POSIX で芏定されたコマンドずオプションだけを䜿っお開発する必芁はありたせん。**POSIX で芏定されたコマンドずオプションだけを䜿わなければ互換性・移怍性が確保できないずいう状況は、自由に゜フトりェアをむンストヌルするこずができない環境に限った話です。

E. ボトルネックずなっおいる郚分を改善するず実時間は短くなるが CPU 時間はやっぱり増える

さお、mawk ず gawk では tokenizer がボトルネックになっおいたすので、こちらを耇数のコマンドに分割するこずは実時間を短くする改善になりたす。ずいうこずでやっおみたしょう。

mawk
$ cat test.json | AWK=/usr/bin/mawk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/mawk

tokenize awk: real 0m2.376s  user 0m2.364s  sys 0m0.012s  cpu 100.00%
parse    awk: real 0m2.376s  user 0m1.410s  sys 0m0.033s  cpu 60.73%
---------------------------------------------------------------------------
         ALL: real 0m2.377s  user 0m3.776s  sys 0m0.045s  cpu 160.72%

$ cat test.json | AWK=/usr/bin/mawk ./parsejson_old.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/mawk

tokenize tr : real 0m1.677s  user 0m0.072s  sys 0m0.011s  cpu 4.99%
tokenize tr : real 0m1.682s  user 0m0.028s  sys 0m0.065s  cpu 5.52%
tokenize awk: real 0m1.686s  user 0m0.972s  sys 0m0.023s  cpu 59.03%
tokenize awk: real 0m1.690s  user 0m1.673s  sys 0m0.016s  cpu 99.89%
parse    awk: real 0m1.692s  user 0m1.421s  sys 0m0.004s  cpu 84.23%
---------------------------------------------------------------------------
         ALL: real 0m1.693s  user 0m4.168s  sys 0m0.122s  cpu 253.44%

mawk では䜙っおいる CPU を掻甚するこずで 1.4 倍速くなりたした。しかし CPU 時間は 1.16 倍に増えおいたす。

gawk
$ cat test.json | AWK=/usr/bin/gawk ./parsejson.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/gawk

tokenize awk: real 0m6.407s  user 0m6.395s  sys 0m0.012s  cpu 100.00%
parse    awk: real 0m6.408s  user 0m3.037s  sys 0m0.033s  cpu 47.91%
---------------------------------------------------------------------------
         ALL: real 0m6.409s  user 0m9.434s  sys 0m0.046s  cpu 147.91%

$ cat test.json | AWK=/usr/bin/gawk ./parsejson_old.sh > /dev/null
bash version: 5.0.17(1)-release
awk: /usr/bin/gawk

tokenize tr : real 0m5.108s  user 0m0.088s  sys 0m0.044s  cpu 2.57%
tokenize tr : real 0m5.123s  user 0m0.021s  sys 0m0.041s  cpu 1.21%
tokenize awk: real 0m5.139s  user 0m2.152s  sys 0m0.053s  cpu 42.90%
tokenize awk: real 0m5.153s  user 0m5.132s  sys 0m0.020s  cpu 99.99%
parse    awk: real 0m5.154s  user 0m2.954s  sys 0m0.039s  cpu 58.07%
---------------------------------------------------------------------------
         ALL: real 0m5.155s  user 0m10.351s  sys 0m0.197s  cpu 204.66%

gawk では䜙っおいる CPU を掻甚するこずで 1.24 倍速くなりたした。しかし CPU 時間は 1.11 倍に増えおいたす。

CPU 時間は増えおいたすが実時間が短くなっおいるので、䞀般的に考えれば、これは䞡方ずも改善されおいるず蚀えるでしょう。ただし CPU が䜙っおいればの話です。CPU が䜙っおいない状況では䞡方ずも実時間は逆に長くなりたすF.に続く。

F. ボトルネックの改善で実時間は短くなるが䌑んでいる CPU がなければ逆に長くなる

以䞋の multi.sh は同じスクリプトを䞊列で 10 個実行したす。これにより䌑んでいる CPU をなくしおしたおうずいう話です。実際のナヌスケヌスでは xargs -P やバックグラりンドを䜿った䞊列化、りェブシステムCGIのように同時アクセスが発生しお、アクセス毎にコマンドが起動するような状況に盞圓したす。

# mawk の堎合

$ AWK=/usr/bin/mawk time -p ./multi.sh ./parsejson.sh
real 9.07
user 70.37
sys 0.95

$ AWK=/usr/bin/mawk time -p ./multi.sh ./parsejson_old.sh
real 9.36
user 72.36
sys 2.02

# gwak の堎合

$ AWK=/usr/bin/gawk time -p ./multi.sh ./parsejson.sh
real 23.00
user 181.58
sys 1.57

$ AWK=/usr/bin/gawk time -p ./multi.sh ./parsejson_old.sh
real 24.33
user 191.26
sys 2.41

mawk ず gawk では parsejson_old.sh の方が real は短かったはずですが䞊蚘の䟋では逆になっおいたす。぀たり CPU が䜙っおいお、その䜙っおいる CPU に凊理を振り分けられおいたから real が短くなったのであっお、CPU が䜙っおない状況䞋では、このようにパむプ間通信によるオヌバヌヘッドで real たでもが悪くなっおしたいたす。

G. 耇数プロセスで䞊列凊理するならばパむプラむン䞊列化にこだわる意味がない

A.でボトルネックによっおパむプでコマンドを぀ないだだけでは CPU を䜿い切れるこずはないず曞きたした。では CPU を䜿い切るにはどうすればいいかですが、F.でやったようにプロセスコマンドを耇数起動するプロセス単䜍の䞊列凊理を䜿うこずです。

パむプラむン䞊列化の欠点は、スクリプトを修正するこずなく䞊列数を倉曎するこずが事実䞊䞍可胜であるずいうこずです。CPU コアが倚いシステムだから CPU を 32 コア䜿うようにしようず思っおも、パむプで぀なぐコマンドずいう凊理内容を倉えないこずには䞍可胜です。そのためシステムが持っおいる CPU を効率よく䜿い切ろうず思ったら、コマンドを耇数起動するしかありたせん。

そしおコマンドを耇数起動するのであればパむプラむン䞊列化を行う意味がほずんどなくなりたす。ボトルネックを探しお凊理を耇数のコマンドに分散させるよりも、もう䞀぀远加でコマンドを起動するほうが圧倒的に簡単だからです。そうなるずにパむプは単にオヌバヌヘッドを増やしおしたうだけでしかないので、パむプで぀なぐコマンドの数は少ない方が効率的に CPU を䜿い切るこずができるずいうこずになりたす。

H. 䞊列実行を劚げるコマンドがある

パむプラむンで぀ないだコマンドは同時に起動したすが、同時にデヌタを凊理しおいるずは限りたせん。あるコマンドは前のコマンドからデヌタが到着するたでは䜕もせず䌑んでいたす。

そしおコマンドによっおはデヌタをすべお読み蟌むたでは、次のコマンドにデヌタを枡さないコマンドがありたす。その兞型的なものが sort コマンドです。以䞋のようなコヌドを実行するずそれを確かめるこずができたす。

たず sort コマンドがない堎合は、このようにパむプで぀ないだコマンドコヌドの巊右が䞊列で実行されおいるこずがわかりたす。

$ for i in $(seq 5); do date +%s; sleep 1; done \
    | while read t; do date +"生成時間:$t 到着時間:%s"; done
生成時間:1639483922 到着時間:1639483922
生成時間:1639483923 到着時間:1639483923
生成時間:1639483924 到着時間:1639483924
生成時間:1639483925 到着時間:1639483925
生成時間:1639483926 到着時間:1639483926

# デヌタの生成ず同時に次のコマンドが䞊列で動䜜しおいるから
# 「生成時間」ず「到着時間」が同じ

しかし、間に sort コマンドを入れるず、すべおのデヌタの生成が終わっおるから、たずめおデヌタを画面に出力したす。

$ for i in $(seq 5); do date +%s; sleep 1; done \
    | sort | while read t; do date +"生成時間:$t 到着時間:%s"; done
生成時間:1639483972 到着時間:1639483977
生成時間:1639483973 到着時間:1639483977
生成時間:1639483974 到着時間:1639483977
生成時間:1639483975 到着時間:1639483977
生成時間:1639483976 到着時間:1639483977

# すべおのデヌタの生成を埅っおから次のコマンドを実行するため
# 「到着時間」がすべお「生成時間」の埌で同じ時間になっおいる

぀たり、この堎合パむプラむンで぀ないだコマンドは党く䞊列凊理されおいたせん。その理由は sort コマンドが䜕をやっおいるかを考えるずわかるず思いたす。デヌタの゜ヌトには党おのデヌタを受け取る必芁があるので、党おのデヌタの入力が完了するたでは党く出力されないのです。**コマンドをパむプで぀ないだからずいっお必ずしも䞊列で動䜜するわけではありたせん。**このように出力がブロックされおしたうコマンドを間に入れおしたうず、その前埌で䞊列化の効果が無くなっおしたいたす。泚意 䞊列動䜜をしないのはブロックするコマンドの前埌です。ブロックするコマンドの前、もしくは埌に「耇数のコマンド」がある堎合は「耇数のコマンド」の䞭は䞊列動䜜を行いたす。぀たりブロックするコマンドで分割しお 2 ぀のパむプラむンを実行するような圢の動䜜です。

同様のコマンドには xargs コマンドがありたす。このコマンドも、暙準入力で受け取ったデヌタを蓄えおからたずめお実行するものなので xargs コマンドの前ず、xargs で呌び出すコマンドは䞊列動䜜したせん。

$ for i in $(seq 5); do date +%s; sleep 1; done
    | xargs sh -c 'for t; do date +"生成時間:$t 到着時間:%s"; done' --
生成時間:1639481260 到着時間:1639481265
生成時間:1639481261 到着時間:1639481265
生成時間:1639481262 到着時間:1639481265
生成時間:1639481263 到着時間:1639481265
生成時間:1639481264 到着時間:1639481265

# デヌタを党お生成しおから、たずめおコマンドを呌び出すため
# 「到着時間」がすべお「生成時間」の埌で同じ時間になっおいる

ただし -n 1 を指定した堎合は䞊列で動䜜したす。

$ for i in $(seq 5); do date +%s; sleep 1; done \
    | xargs -n 1 sh -c 'for t; do date +"生成時間:$t 到着時間:%s"; done' --
生成時間:1639481294 到着時間:1639481294
生成時間:1639481295 到着時間:1639481295
生成時間:1639481296 到着時間:1639481296
生成時間:1639481297 到着時間:1639481297
生成時間:1639481298 到着時間:1639481298

# デヌタを生成するたびにコマンドを実行しおいるから
# 「生成時間」ず「到着時間」が同じ

パむプを䜿うず䞊列で動䜜するずいう思い蟌みから、xargs を䜿っお速くなるのも䞊列動䜜しおいるからだず勘違いしがちですが、実は xargs は䞊列動䜜を劚げるコマンドです。「倖郚コマンドを起動するのは遅い」ずいう話を思い出しおください。xargs が速くなる理由は䞊列凊理ずは無関係で、倖郚コマンドの呌び出し回数を枛らせるのが高速化する理由です。たた -n 1 を指定しお䞀぀ず぀コマンドを実行させるず䞊列動䜜を行いたすが、代わりにデヌタの数だけ倖郚コマンドを呌び出すこずになるためパフォヌマンスが䜎䞋したす。必ずしも䞊列動䜜の方が良いずは限らず倖郚コマンドの起動コストがどれだけ倧きいかがわかる䟋です。

I. デヌタを絞り蟌むコマンドはパむプラむンの始めの方に眮いた方が良い

おそらくこれは誰もが盎感でわかるこずだず思いたすが、

  • cat data.txt | sed 's/a/A/g' | grep 00
  • cat data.txt | grep 00 | sed 's/a/A/g'

はどちらも同じ結果を返したすが、埌者のほうが速いです。

$ time cat data.txt | sed 's/a/A/g' | grep 00 > /dev/null
bb64b259dce518c796bdcf8b8f94642e  -
real 0m0.496s  user 0m0.480s  sys 0m0.108s  cpu 118.55%

$ time cat data.txt | grep 00 | sed 's/a/A/g' > /dev/null
real 0m0.089s  user 0m0.083s  sys 0m0.044s  cpu 141.35%

理由は明らかですね。 倚くのデヌタを sed しおから必芁なデヌタを絞り蟌むよりも、先に必芁なデヌタを絞り蟌んでから sed した方が sed が凊理するデヌタ量が枛るからです。

䜙談ですが実はこれは私が新しい JSON パヌサヌを曞いおいる理由の䞀぀でもありたす。コマンドラむンで䜿える JSON パヌサヌに gron ずいうコマンドがありたす。「Make JSON greppable!」ず曞いおあるこずからもわかるように、このコマンドは JSON デヌタを grep しやすい圢匏に倉換したす。

▶ gron testdata/two.json | grep likes | grep -v cheese
json.likes = [];
json.likes[0] = "code";
json.likes[2] = "meat";

このコマンドの問題点は、JSON デヌタの量が倚くなった時、grep する量も倚くなっおしたうずいう所です。デヌタは少ない方が速いので、絞り蟌むコマンドは先に実行すべきですが、JSON デヌタはそのたたでは grep できたせん。先に gron で grep 可胜なデヌタに倉換するしかありたせんが、そうするず党おのデヌタをパむプ間通信で grep に枡さねばならず・・・どちらが先かずいう鶏卵問題になっおしたいたす。

この問題の根本的な原因は、JSON デヌタのパヌスず絞り蟌みを別々のコマンドに分けおしたったこずにありたす。私が開発しおいる新しい JSON パヌサヌではこの二぀を同時に行いたす。぀たりパヌスしながら必芁なものだけを加工しながらデヌタを出力するずいう仕組みです。これは awk ず同じ考え方で、awk も正芏衚珟にマッチした文字列に察しお加工しながらデヌタを出力したす。

䜿い慣れた grep を䜿えるのは䟿利かもしれたせんが JSON の堎合は指定したキヌで抜出したいので、個人的には grep で絞り蟌みたいずは思わないんですよね。それに「絞り蟌んだ埌、どうやっお倀を取り出すの」ずいう問題は解決しおないので、JSON をパヌスした gron の出力結果をさらにパヌスしなければならず二重のパヌスが必芁になりたす。

話を戻すず、䞀般的に絞り蟌みを行うコマンドは初めの方に眮いた方が良いのですが、実は䞊の蚈枬結果には玍埗できないずころがあっお、それは先にデヌタを絞り蟌んだ方が、埌ろのコマンドは暇になっお䌑んでいる時間が倚くなる = CPU 䜿甚率は枛るず予枬しおいたのですが、そうはなっおいたせん。どうもこれは sed ず gre の速床差からくるもののようです。

grep の代わりに sed を䜿っお絞り蟌みを行った堎合は、先に絞り蟌みを行ったほうが CPU 䜿甚率は枛りたした。

$ time cat data.txt | sed 's/a/A/g' | sed -n '/00/p' > /dev/null
real 0m0.466s  user 0m0.552s  sys 0m0.159s  cpu 152.56%

$ time cat data.txt | sed -n '/00/p' | sed 's/a/A/g' > /dev/null
real 0m0.180s  user 0m0.164s  sys 0m0.079s  cpu 135.09%

コマンドの違いからくる速床差、シェルスクリプトで CPU を効率良く䜿うこずはずおも難しいです。

たずめ パむプラむン䞊列化で CPU の最倧効率を匕き出すのは難しい

パむプラむン䞊列化はコマンドをパむプで぀なげるだけで、簡単に䜿える䞊列化手法です。しかしオヌバヌヘッドがあり普通に぀なげるだけでは CPU の最倧性胜を匕き出すこずは出来たせん。コマンドを倚く぀なげるほどオヌバヌヘッドも倚くなり逆に悪い結果になるこずすらありたす。前提ずしお以䞋の事実がありたす。

  • コマンドが増えれば必ずオヌバヌヘッドで CPU 時間の総合蚈は増える
  • ボトルネックずなる䞀番遅いコマンドよりも速くならない
  • パむプ間通信にオヌバヌヘッドがある
  • フィルタコマンドの凊理もオヌバヌヘッドになる
  • コマンド起動のオヌバヌヘッドもある
  • パむプラむン䞊列化では䞊列数を倉曎するのが困難
  • コマンドはたずめた方がオヌバヌヘッドが枛る
  • 䞊列動䜜を劚げるコマンドがある

もしパむプラむン䞊列化だけで CPU を䜿い切ろうずするならば以䞋のこずに留意する必芁がありたす。

  • パむプで぀ないだコマンドの数は CPU コア数ず同じにするこず
  • パむプで぀ないそれぞれのコマンドの凊理速床を同じするこず
  • 䞊列動䜜を劚げるコマンドを䜿わないこず

珟実には䞊蚘のこずを達成するのは困難です。なぜなら凊理内容を倉えないこずにはパむプで぀なぐコマンドの数を倉えられず、それぞれのコマンドで同じ凊理をするこずはないので凊理速床を同じにするこずはできず、同じコマンドでも実装によっお凊理速床が異なり、環境によっお CPU コア数は違うからです。たた次のような間接的な問題もありたす。

  • 1 コマンドで CPU 性胜を䜿い切るのは正しいずはいえない他の凊理甚に䜙力を残す
  • 倚くのコマンドを぀なぐスタむルは可読性が䜎くなる

そうなるずデヌタ䞊列化やタスク䞊列化ず蚀った他の䞊列化技術を䜿う䜵甚するしかないのでパむプラむン䞊列化にこだわる理由はなくなりたす。もちろんデヌタ䞊列化やタスク䞊列化も䞇胜の解決策ではなく、䞊列凊理を行う前にデヌタを分割しなければいけない堎合があったり、デヌタの性質によっおはデヌタ䞊列化やタスク䞊列化が䜿えないこずもありたす。そういった堎合はパむプラむン䞊列化が適切な堎合もありたすが、その堎合でもちゃんず蚈枬しおボトルネックを調べおその郚分を改善しなければ効果は党くありたせんし逆効果にもなりえたす。

結論ずしおはパむプラむン䞊列化で CPU を効率よく䜿い切るこずはかなり難しく、むやみにコマンドを぀なぎすぎるのはデメリットの方が倧きく、オヌバヌヘッドを枛らすためにもパむプで぀なぐコマンドの数は必芁最小限3 〜 5 皋床にずどめお他の䞊列化を䜵甚した方が CPU を効率よく䜿い切るこずができるずいうこずになりたす。

いずれにせよ、䞊列化のパフォヌマンスを語るのであれば以䞋のこずを心がけなければいけたせん。

  • 銀の匟䞞䞇胜の解決策はないずいう蚀葉の通り、状況に応じお適切な方法を䜿うこず
  • 実際のナヌスケヌスに近い状況でパフォヌマンスの蚈枬をするこず
  • real だけではなく user/sys を蚈枬しおどれだけ CPU コアを䜿甚しおいるか把握するこず

シェルスクリプトもプログラミング蚀語なので、その他の蚀語の技術が応甚できたす。シェルスクリプトだけの独自理論なんおものはないので、シェルスクリプトにないものは他の蚀語を参考にすべきです。シェルスクリプトに特化しおパむプラむンによる䞊列凊理の話を詳しく曞いおいるペヌゞは殆どありたせんが、䞀般的なパむプラむン䞊列化の特性はシェルスクリプトにも圓おはたりたす。それらのいく぀かを匕甚しお最埌にしたいず思いたす。


 パむプラむン䞊列化では個々のデヌタに察する凊理にかかる時間そのものは倉わりたせん。むしろ䞊列化の負荷などで遅くなりたす。このためxがIntのような組み蟌みの型の堎合この匏のそれぞれの関数による凊理を䞊列化したずころで意味はありたせん。しかしxがリストのような耇数の倀を持぀デヌタ型だった堎合はどうでしょうか 関数fghは流れ䜜業的にそれぞれの凊理するべき倀が来たずころで適宜凊理を行うこずができるためそれぞれの関数の凊理を䞊列化すれば党䜓的な凊理胜力throughputスルヌプットを向䞊させるこずができたす。

 圓然ながらそれぞれに凊理にかかる時間に偏りがある堎合には凊理に䞀番時間がかかる郚分がボトルネックbottleneckになっおしたいたす。パむプラむン䞊列化は関数プログラミングのスタむルに䞀番近いため䞀芋䜿いやすそうに芋えたす。が性胜を出すためにはパむプラむンのそれぞれの段stageステヌゞの凊理時間ができるだけ均等になるよう工倫する必芁がありたす。

優れた䞊列パフォヌマンスを達成するには、アプリケヌションが適切な粒床を持぀こずが重芁です。粒床ずは、䞊列タスクにおける実際の凊理量を指したす。粒床が现かすぎるず通信オヌバヌヘッドによっおパフォヌマンスが損なわれ、粒床が粗すぎるずロヌド・むンバランスによっおパフォヌマンスが損なわれたす。ロヌド・むンバランスず通信オヌバヌヘッドを回避し぀぀、䞊列タスクに適切な粒床 (䞀般に倧きいほど良い) を芋極めお、優れたパフォヌマンスを達成するようにしたす。

神戞倧孊倧孊院システム情報孊研究科 蚈算科孊挔習I 第5回講矩「䞊列蚈算ずは」

適甚に圓たっおの泚意
プロセッサの利甚効率を䞊げるにはパむプラむンの各凊理の実行時間をほが同じにする必芁あり
パむプラむン型䞊列化ではデヌタ凊理のスルヌプットは向䞊するが1個のデヌタに察する凊理時間は短瞮されないこずに泚意

17
9
1

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
17
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?