なんかすごく前に話題になっていたので、確かめてみました。
数値計算に強いプログラミング言語と言えば従来FortranとC言語でしたが、近年Pythonのように書けて実用上十分な速度を達成できるJuliaが人気を集めているようです*1。
Juliaは動的言語ですが、(1) 型推論を行うためfor文などでいちいち型検査するPythonより速い (2) JITコンパイルされるので関数に切り出した場合に高速、といった点が高速化に寄与しています。
「C言語で書いたほうが速いに決まっている」などの決めつけはよくないので、実際に計測してみます。
そもそもベンチマークのソースコードが提示されていないので議論のしようがないのですが、「浮動小数点数を足すだけ」という文章を私が解釈したコードは以下の通りになります。
function test() a = zeros(Float64, 1000) a[1] = 1e-16 sum = 1.0 for j = 1:10000000, i = 1:1000 sum += a[i] end return sum end test()
double test() { double a[1000] = {}; a[0] = 1e-16; double sum = 1.0; for( unsigned long long j = 0; j < 10000000; ++j ) { for( unsigned long long i = 0; i < 1000; ++i ) { sum += a[i]; } } return sum; } int main() { return test(); }
メモリアクセスの速度が測りたいわけではないので、配列のサイズはL1キャッシュに乗る程度の大きさとしました。
環境
- gcc 9.1.0、
-O3
をつけてコンパイル - julia version 1.5.2
- Intel(R) Xeon(R) CPU E5-2603 v3 @ 1.60GHz(addsd命令は3cycleレイテンシ)
結果
C言語
- 30.1G cycles
- 35.1G instructions
- 5.0G branches
- 18.8 seconds
Julia
- 31.9G cycles
- 61.5G instructions
- 20.3G branches
- 19.1 seconds
考察
数値上はJuliaのほうがほんのわずかに遅くなっていますが、C言語は事前コンパイルが必要でそれに0.2秒ほどかかることを考慮に入れると速度は同じと言ってよいでしょう。
そもそもこのベンチマークの速度はレイテンシに支配されているため、それなりの最適化がかかっていれば速度が変わるわけはないのです。
C言語コードをコンパイルした結果は以下のようになっており、ループ一周は7命令です。アセンブリ上でのループ一周は元のソースコードの二周分に相当しています(アンローリングされています)。
この中には直列に依存したaddsd命令が2つあるので、ループ一周当たり6cycleかかることになります。
C言語をコンパイルした結果のperf
のサイクル数、命令数、分岐命令数は、これらと整合します。
.L3: movsd xmm1, QWORD PTR [rax] add rax, 16 addsd xmm1, xmm0 movsd xmm0, QWORD PTR [rax-8] addsd xmm0, xmm1 cmp rdx, rax jne .L3
一方Juliaの方はサイクル数こそほぼ一緒ですが、命令数がかなり多くなっています。
これは配列の境界検査を毎回行っているためです。
C言語と条件をそろえ、配列境界検査を行わないようにするため、@inbounds
をつけてみると、以下のように命令数と分岐命令数が減ります。
- 31.9G cycles
- 41.5G instructions
- 10.3G branches
- 19.1 seconds
命令数は一ループ当たり2命令、分岐命令数は一ループ当たり1命令減りました。この減った分は、比較命令と分岐命令ということになります。
分岐命令数から察するにループアンローリングは行われていないようですが、Juliaで書いたコードをJITコンパイルした結果はほぼ最適です。
まとめ
「浮動小数点数を足すだけのプログラム」の速度がC言語で書いた場合とJuliaで書いた場合で変わることはありませんでした。