PLINQは黒歴史
PLINQ (Parallel LINQ) は黒歴史かも知れない。
私はLINQが好きだ。愛しているといっても過言ではない。
着実な言語的積み重ねの上に成り立つ、これほどスマートな改善を見たことがない。
LINQを知らない人のために簡単に説明しよう。(知っている人は次のコードサンプルを読み飛ばすこと。)
LINQは関数型にヒントを得た……などは置いておくと、要はfor文の組み合わせたデータ処理を簡単にしてくれるものだ。
従来、配列のデータをフィルタしたり集計するには、forブロックと中間変数が必要だった。
たとえば、Personクラスの配列から、20代だけを抽出し、年収平均を計算したいとする。
// 処理データ集合を確定する。
Person persons = new {…}; // データは与えられているものとする。
// 対象を絞り込む。
List<Person> targets = new List<Person>();
foreach(var p in persons) {
if(20 =< p.Age && p.Age < 30) {
targets.Add(p);
}
}
// 合計年収を計算する。
double incomeSum = 0;
foreach(var t in targets) {
incomeSum += t.Income;
}
// 平均年収を計算する。
double incomeAvg = incomeSum / targets.Count();
となる。ああ、長い。
LINQだと、こうなる。
// 処理データ集合を確定する。
Person persons = new {…}; // データは与えられているものとする。
// 平均年収を計算する。
double incomeAvg = persons.Where(p => 20 =< p.Age && p.Age < 30).Average(t => t.Income);
素晴らしい。従来は「ブロック」を記述して長くなっていたことが、「文」で書けるようになった。スマートに。
このように、LINQは、生産性と可読性を一気に向上する素晴らしい(大事なことなので二度いうほどの)機能である。
さて、このLINQをマルチコア対応しようとして作られたLINQ拡張がPLINQである。
LINQは、Where() などの計算をシングルスレッドで処理するが、これをマルチスレッドでやれば、速くなるんじゃね?というものだ。
当初から「なんかうさんくせーな」と思っていて、実際使ってみたら「なんじゃこりゃ」だったのだが、今日ちょっと測ってみた。
処理内容は、3,000,000個の数値のフィルタと計算。こんな感じ。
var source = Enumerable.Range(0, 3000000)
.ToArray() // disable for Range measuring
;
var result = source
.AsParallel() // enabled on PLINQ
.Where(num => num != 0) // enabled on LINQ, PLINQ
.Select(num => num + num) // enabled on LINQ, PLINQ
;
で、これを for なり foreach で反復したときの処理時間を計った(10回のうちの最速値)。
配列 [ms] | Range [ms] | |
for (int index =0; … | 175 | - |
foreach & if文 | 175 | 313 |
foreach & LINQ | 455 | 637 |
foreach & PLINQ | 2411 | 2209 |
# うちはCore2 Quadです。
まず、配列に対しての処理について。
やはり、この手の計算では単純 for が最速。foreach&if が等速なのが意外だが、これはコンパイラがforと等価に変換しているためだと思う。
LINQは良く健闘している。3倍くらいに処理時間が延びているが、LINQの目的は生産性と可読性の向上なので、この性能劣化が許容できるならべつに問題ではない。(3,000,000個を処理するルーチンは、普通のソフトウェアにはあまりないだろう。したがって、大部分でLINQは良い選択肢になると思う。)
最後にPLINQ。お前はダメだ。PLINQの目的はLINQを並列化して高速にすることだったはず。なのに、LINQより時間が掛ってどうする。シングルコアforと比べて13倍遅いとはどういうことだ。PLINQ、お前の存在意義はなんなんだ。最悪なのは、LINQと違ってCPUコア4個を全部もってくってことだ。信じられない。1コアあたりの計算効率は1/52だ。俺のCPUが40MHzくらいになったのと同じってことだ。マジ消えていい。
追試で、WithDegreeOfParallelism(1)をしたら 1462[ms] になった。PLINQのくせにパラレルにしないほうが速いとは何の冗談だ。
表の Range は、sourceのToArray()を消した版。同じ傾向になった。
以上の結果より。PLINQは役に立たないということが明らかになった。
いや、役に立つことはあるんだろう。今回の試験ではタスク粒度が細かすぎる。もう少し「良い感じの計算量」で「並列度の高い」タスクを処理すれば、、、、。……そんなん都合良く出てこないがな。だいたいタスク粒度が一定を超えたらスレッドとかTaskにしちゃってLINQなんか使わんわ。じゃないとコードが読みにくくなるわ。
ちょっと前、マルチコアだメニーコアだ、実装を知らない人たちが何かに浮かされたように並列化に取り組んだときがあったよね。(今も?)
PLINQもその産物なんだろう。机上技術的にはPLINQの思想は理解できる。
でも実践としては今のところ害でしかない。手を出しちゃいかんね。