繰り返しのコードを書いたら負け

Mathematica の繰り返しがおもしろかったので,ちょっと関連する話をしてみようと思います.

結論から言うと,「繰り返しのコードを書いたら負け」ということです.高校生が行列やベクトルを勉強する時に,「成分書いたら負け」と教わるのと同じ感じです.簡単な例を使って調べてみましょう.

100万個の乱数からなるリストxListを作り,各要素のSinを求めます.いろんな方法が考えられますが,Sinの計算にかかる時間はピンキリです.AbsoluteTimingで計ってみましょう.

Core i7 930(HTはOff), 主記憶6GB,Windows 7 64-bit上のMathematica 8.0.0で試します.

空のリスト+繰り返し

まずはやってはいけない例から.空のリストに結果を追加するような方法は,時間がかかりすぎます.バージョン6で導入された添字を使わない方法(いわゆるforeach形式)と,従来からある添字を使う方法,どちらも測定をあきらめました.

AbsoluteTiming[
 result = {};
 Do[AppendTo[result, Sin[x]], {x, xList}];]

$Aborted
AbsoluteTiming[
 result = {};
 Do[AppendTo[result, Sin[xList[[i]]]], {i, 1, Length[xList]}];]

$Aborted

固定サイズのリスト+繰り返し

格納するためのリストのサイズを最初に決めておくようにすれば,現実的な時間で計算できます.添字を使わない方法と添字を使う方法との間に,性能の違いはほとんどありません.

AbsoluteTiming[
 result = Table[0, {Length[xList]}];
 Module[{i = 1}, Do[result[[i++]] = Sin[x], {x, xList}]];]

{3.3941941, Null}
AbsoluteTiming[
 result = Table[0, {Length[xList]}];
 Do[result[[i]] = Sin[xList[[i]]], {i, 1, Length[xList]}];]

{3.3291904, Null}

リストの生成,逐次計算

リストを生成するための関数であるMapTableを使えばかなり早くなります.Mapを使う方法と添字を使うTableの性能は同じですが,添字を使わないTableは,かなり性能がよさそうです.

AbsoluteTiming[result = Map[Sin, xList];]

{0.1320076, Null}
AbsoluteTiming[result = Table[Sin[xList[[i]]], {i, 1, Length[xList]}];]

{0.1320076, Null}
AbsoluteTiming[result = Table[Sin[x], {x, xList}];]

{0.0940054, Null}

リストの生成,並列計算

バージョン7で導入された組み込み並列計算のためのParallelTableParallelMapは,Sinのような簡単な計算では威力を発揮できません.

AbsoluteTiming[result = ParallelTable[Sin[x], {x, xList}];]

{1.6220928, Null}
AbsoluteTiming[result = ParallelMap[Sin, xList];]

{1.0480599, Null}
AbsoluteTiming[result = ParallelTable[Sin[xList[[i]]], {i, 1, Length[xList]}];]

{0.5030287, Null}

添字を使う方法と添字を使わない方法の性能差が,逐次計算の場合と逆になっています.

Listable+ベクトル化

実を言うとSinは,引数がリストなら自動的に各要素に適用されるListableな関数なので,MapTableも不要です.さらに,バージョン5.2で導入された自動ベクトル化をサポートしているので,次のように単純に書くと圧倒的な速さになります.ベクトル化をちゃんとサポートしたコンパイラを使わないと,CやC++でもこの速度は出ないのではないでしょうか.

AbsoluteTiming[answer = Sin[xList];]

{0.0080005, Null}

というわけで,最初に書いたとおりの結論,「繰り返しのコードを書いたら負け」になります.