これからのプログラマーに『並列処理』の知識は必須か?
「CPU単体の処理性能の向上が限界に来ている」という話をよく聞くようになりました。
昔はCPUがPetium2 → Petium3 → Petium4とバージョンアップされる度にクロック周波数が266MHz(メガヘルツ)から533MHzになるなど、常に上がり続けてきましたが、最近は3GHzとか4GHzくらいで止まっていますよね。
ムーアの法則で「シリコン集積回路の集積密度が2年でほぼ2倍になる」と以前は言われていました。かんたんに言うとコンピュータの性能は2年間で2倍にアップするという法則です。
しかし、CPUがあまりに高度に集積されるようになり、これ以上単体での性能アップは無理だという段階になってしまったのです。
その分CPUのコア数が増えて、Core2Duoでコアが2つになり、Core-i5で4つのコアが搭載されるようになりました。その後もヘキサコア(コアが6つ)、オクタコア(コアが8つ)と増え続けています。
周波数は伸びなくても、コア数が増えているので、複数の処理を複数コアで同時並行で処理することによってスピードアップを図っているんです。
よって、CPUのマルチコア化に対応したプログラムの書き方が出来ないとプログラムの処理性能を上げることは出来ないと言われています。
この点について、じっくり考えてみたことを紹介します。
Contents
Ruby開発者まつもとゆきひろさんの意見
まつもとゆきひろさんは「ソフトウェア開発にフリーランチは存在しない」とおっしゃっています。
フリーランチってタダ飯のことです。
今まではプログラムを一度書けば、ハードウェアの性能が上がるから年々勝手に動作速度が速くなっていくというタダ飯(プログラムを何も修正しなくても速くなる)をいただけました。
しかし、現在ではCPU単体の処理性能が限界に達したため、並列に動作するように書き直さないと処理性能が上がっていかなくなってしまったのです。
このことは、ここ数年、関数型言語が注目されていることに関係があります。
関数型言語は並列処理に向いている?
関数型言語は副作用のないプログラミングがしやすいという特徴があるため、並列処理がしやすいんです。
副作用のないプログラミングとはどういうことかというと、関数の動作が関数の外側にある変数に依存しないということです。以下はJavaScriptのサンプルコードです。
//副作用のないコード function goukei(num1, num2) { var sum = num1 + num2; return sum; } var result = goukei(198, 246); //副作用のあるコード(関数goukei2は外側にある変数sumに依存している) var sum = 0; function goukei2(num1, num2) { sum = num1 + num2; return sum; } var result = goukei2(198, 246);
オブジェクト指向でいうと、クラスのメンバー変数(フィールド)をメソッドから参照しているようなコードは副作用のあるコードなんです。
状態を持った変数によって、関数の動作が変わってしまう場合、その関数を並列で実行することは出来ません。
しかし、関数が引数にだけ依存している場合は、その関数を並列で処理することが出来ます。
なぜならば、引数とローカル変数はその関数内でしか参照されないからです。
例えば、for文の中でfunc1という関数を呼び出しているとします。この時、func1の動作が外部の変数に依存しないのであれば、for文を順番に繰り返すのではなく並列で処理することが出来ます。
//副作用のないコード function goukei(num1, num2) { var sum = num1 + num2; return sum; } var nums = { { num1:198, num2:246 }, { num1:753 num2:42 }, { num1:464, num2:1365 }, { num1:61, num2:322 }, }; for (var i=0; i < nums.length; i++) { // ここ↓を並列で実行出来る?(i++を順番に処理しなきゃいけない気もするけど。概念的にはこんな感じの話。) var result = goukei(nums[i].num1, nums[i].num2); }
関数型言語で書くと、外部の状態変数に依存しない書き方になりやすいので、プログラマーが並列を意識してコードを書かなくても、言語の処理系が自動的に並列に実行できる部分は並列に実行してくれるんです。
例えば、for文で4回処理を繰り返していて、CPUコアが4つある場合、くりかえし一回の処理を1コアに割り当てれば、4つコアがあるので、4回の繰り返しを並列に処理できる場合があるのです。
意識的に並列処理を書くとかなりめんどい
プログラマーが意識して並列処理をさせるコードを書く場合は、for文の中の処理をひとつひとつThred起動して実行させればいいのですが、並列で起動したスレッドが全部終わるのを待つjoin処理なども書かなければなりません。
先程のJavaScriptのコードをJavaで並列に実行するコードを書いた例を示すと以下のようになります。(new Thread(num1, num2)の部分が正確ではないコードです。簡略化してあります)
int[] nums = { { 198, 246 }, { 753, 42 }, { 464, 1365 }, { 61, 322 } }; Thead[] threds = new Thead[nums.length]; for (int i=0; i < nums.length; i++) { Thread th = new Thread(num1, num2) { public void run() { goukei(num1, num2); } }; th.start(); } for (int i=0; i < threds.length; i++) { th.join(); }
コードも複雑になりますし、可読性も落ちます。
サーバーは元々並列処理してるからプログラムはそのままでOK
WebアプリやJSON APIなどサーバーサイドのプログラムの場合、1リクエストを処理するプログラムを私たちは書きますよね?では、その1リクエストを処理するプログラムの中でさらにユーザースレッドを起動して処理を並列に動かす必要があるでしょうか?ないですよね。
なんでかというと、サーバー自体が並列で動いて同時アクセスをさばいているからです。
例えばCPUが8コアあるサーバに10個の同時アクセスがあったとします。
その1リクエストの中の繰り返し処理を並列で8個のCPUコアに割り当てて並列処理をさせることになります。
そんなことをしたら、1リクエストで8コアすべてを使ってしまうので、他のリクエストがCPUを使用できなくなってしまいます(厳密に言えばCPUをタイムシェアリングしてるから、CPU割当が短時間で切り替えられますが)。
このような理由から、サーバーサイドのユーザープログラムの中では、並列処理を意図的に起こす必要性はないと考えられます。サーバー自体が並列でリクエストを処理していますからね。私達が書くユーザープログラムに制御が移る時には、既にプログラムは並列で動いているのです。
具体的に言えば、Apacheからmod-phpだったり、Nginxからphp-fpmに処理が移ってユーザープログラムが起動される時には、既に並列で起動されてるということです。
ですから、サーバーサイドでは並列処理を意識する必要性はないのです。
ただ、クライアントサイドでは状況が違います。
クライアントサイドは並列処理の意識が必要?
私のキャリアはサーバーサイドが中心なのですが、スマホゲームのクライアント側の開発をしたこともあります。
ゲームのクライアント側ってすごく並列的に動いているんですよ。
フレーム処理というものがあって、一秒間に30回画面描画が走ることを30fps(frame per second = フレームレート)と言います。
それくらい細かい描画処理をしながら、裏でサーバーのAPIと通信してたり、ローカルストレージにデータを保存したりします。
ですから、クライアント側は並列を意識してコードを書く場面があると思います。スマホのCPUもマルチコアですからね。
とはいえ、これらの処理はゲームエンジンなどを使えば、ゲームエンジンが下のレイヤーでGPUをうまく使うなどを勝手にやってくれるので、やはり、クライアントサイドにおいても、アプリケーションプログラマーが並列処理を意識する必要性はそれほどないと思います。
まとめ
そんなわけでここ数年並列処理の重要性が説かれていますが、実際にはそういうコードを書く機会は少ないと思われます。
まぁ、私が書いてるコードのほとんどがアプリケーションレイヤーなので、もう少し下のレイヤー(フレームワークとかゲームエンジン)を開発している人だと違った状況なのかもしれませんけどね。