たぶん WebKit のミーティングに行った時に、 GPU rendering のトピックがとても盛り上がっていて、 morita さんが「GPUとか「ちょっとオレも一言言わせろ」って感じで盛り上がっちゃうテーマだよね」と言っていて、不思議なくらいなるほどーと思った記憶があります。何が言いたいかというと、並列とかそういうテーマですよね、と。ということで、一言というか、色々と言いたいことがあるのです。
バカパラサイコーという話
Embarassingly parallel をバカパラと訳したの、どなたが考えたのか知らないけど、とても好き。ほぼ独立したタスク群であれば自明にコアを使い切れるし、その数だけ速度がスケールする、という話。 MapReduce の mapper 、ピクセルシェーダ、並列ビルド、深層学習モデルのデータパラレルもそう。普通同じタスクが違うデータに対して動く時にバカパラと言う気もするけど、独立しているから並列化可能、という意味で並列ビルドも同じ箱に入れて考えている。 kzys さんの話しているようなクラウドのケースも、そもそも物理 CPU を切り売りしていて、コンテナごとに完全に独立なので、これも僕はバカパラ、くらいの認識でいる。
プログラムもとても簡単。プロセス並列でいいなら shell script で
for i in `seq 10`; do
./my_command $i &
done
wait
とかで十分だし、 OpenMP 使うなら #pragma omp parallel
つければいいし、適当なスレッドプールを使うのも、自分で作るのも、そんなに難しくない。
並列プログラムは難しい、でもなんとかなってる気がする
バカパラ以外のケースは、並列 reduce などのよく知られたケースを除くと、なかなか大変なことが多いように思う。特に個々に別々の役割を持ったスレッド・プロセスが協調して動いてるようなやつ。サーバサイドだと、フロントエンドのリクエストを受けて、バックエンドにリクエスト投げたりキャッシュしたりゴチャゴチャやってからレスポンスを返す、ミドルエンド的なやつが大変だった記憶がある(例)。あとは Chrome もなんだかたくさんプロセスもスレッドもあって、大変なところはとても大変な印象だった。 Chrome はブラウザというよりはユーザランドで動いてるマイクロカーネルという認識をしているので、カーネルとかもそうなんだろうなーと思っている。
ここで言う大変というのはバグっていないプログラムを書くのが大変ということで、この手のスレッドプログラミングは、書くのもレビューするのもデバッグするのも難しい。ただ、難しいんだけど、個人的には人類はなんとかなりそうな道具を揃えられたんじゃないかな、と思っている。
morita さんや karino2 さんが紹介していた Future/Promise や、 Go のチャンネルのように、 mutex のような古いプリミティブよりバグりにくい、新しい抽象が出てきたのがひとつ。 Rust のように型レベルでスレッドのバグをコンパイルタイム時に検出する言語もあるし、よく使うロックフリーデータ構造とかのライブラリも整ってきているので、難しい atomic op を直接使う理由はあまり無いと思う。あと何より、その手のものを一切使ってなくても、 ThreadSanitizer が割とバグを見つけてくれる。余談だけど、 sanitizer の類は C++ という言語の寿命を延命させているように感じている。
苦労して書いたコードがスケールしない悲しさ
ただ、それがスケールするかというと、別の話。別々の役割を持ったスレッドがたくさんあるようなプログラムは、理想的な状況でも役割の数以上にコアを使うことはない。クラウドだったら kzys さんのおっしゃる通り、確保する論理コアの数を必要な数程度に減らせばいいだけと思っているけど、クライアントサイドのプログラムでは、 karino2 さんの問題意識のように、単にコアを使いこなせてない、という状況になってしまう。
実際、これは既に起きている問題だと思っている。ハイエンドスマホのコア数が 8 とか越えたのは、もう5年以上前だと思うけど、その後はずっと横這いだと思う。バカパラな用途があるハイエンドはともかく、ミドルエンド PC のコア数もそんなに増えていないと思う。アプリケーションが使わないから、リソースを他に回していると理解している。スマホのコア数競争の当事者であった Qualcomm の人が「コア数増やすのって単に広告競争で、意味ないよねー」と言っていた、という話もある。一方で最初から用途がバカパラのアクセラレータは際限なくコア数を増やしていっている。
コアを使い切れていない時にどうすれば良いかというと、今シングルスレッドで動いている部分にバカパラ並列性を見出せるとてっとり早い。ただ、これができるなら既にやられているはずで、難しいから、意味がないからまだやられていないという可能性も高い。例としてコンパイラを考えてみると、パースに文脈依存がない言語であれば、ここは割と並列化できそうな気がするけど、その後の意味解析などはかなり難しそうだ。シングルスレッドの部分が残るのであれば、そこが律速するので部分的に並列化するうまみは少ない。
別のアイデアとして、細粒度マルチタスクのようなものも考えられる。タスクの依存関係を実行していく、 make みたいなやつってすごくうまく並列化できるので、プロセス内に細かいタスクを大量に作って、依存が解決されたものからスレッドプールに投げ込めば綺麗に並列性を使い切れるのでは、という考えかた。これは頭で考えると、とてもうまくいきそうな気がするんだけど、実際にはあまりうまくいかないことが多いと思う。というのは、タスクを細かく切りすぎると、同期のコストが高くなるのと、データを別のコアに転送するコストがどうしても高くなってしまう。
メニーコア時代到来!ってずっと言ってる気がするよね
同期の方はさまざまな工夫で減らす余地があるのだけど、高速化したいプログラムって本質的にたくさんのデータを扱うものが多いわけで、 NUMA ノードを越えてデータを運ぶ方が計算そのものより時間がかかる、というケースは多い。僕個人としても、複数のスレッドがそれなりに忙しく動いているプログラムにも関わらず、 taskset -c 0
で一つのコアにはりつけた方が速かった、とか、フェーズが複数あるプログラムの各フェーズを別スレッドにしてパイプラインにしてもたいして速くならなかった、みたいな経験はちょくちょくある。
コア数が増えた時に遠いメモリが遅くなるのは、これは物理的にどうしようもない問題だと思うので(この世界に空間が5次元くらいあったらだいぶ伸びしろが残るんだろうけどなあ、とか考えるのは楽しい)、バカパラから遠い並列プログラムはメニーコアを使い切るのは不可能だし、今後コアが増えるとするとますます余っていくと思っている。例えば、ブラウザが 100 コアを効率良く使う未来を僕は想像できない。
というわけで、この世にはバカパラで高速化できるアプリケーションと、あまりたくさんコアを使えないアプリケーションがあり、それに応じてコア数も二極化するんじゃないかな、と思っている。なんかメニーコア時代とか10年以上言ってるわりにはご家庭のコア数は増えてなくて、既にそうなっている気もするけど。なんにでも深層学習が使われている、みたいな状況になると少しは変わるかもしれないけど……なんかそれは CPU よりはアクセラレータにやらせる流れだとは思うし。
ビルゲイツが「メインメモリは 640kB で十分だよ」と言った話のように、 8 コアもあれば十分と言ってたアホがいる、と笑い話になるかもしれないけどね。