Rubyの浮動小数点演算が遅い件


 まず真っ先に書いておくが、Ruby をディスるつもりは全くない。

 以前、風向は単位ベクトル平均を取ることを書いた。気象庁の「風向」の定義は10分平均、航空管制で用いられる「風向」は2分平均が使われるらしいが、USB Weather Board + Weather Meters を用いた観測では2分、10分、60分の平均を計算している。当初、Ruby で集計していたのだが、データ取得のタイミングである 3 秒ごとに実行されるので、 PhenomII X6 1065T だとこの処理だけで (800MHz のままとはいえ) 1 コアを使う程度の負荷がかかっていた。

 もちろん負荷軽減のために単位ベクトル化処理は演算済みのテーブルで変換するだけだったし、平均処理に必要な ∑ の計算は SQLite 上で行なっていた。それでもそれだけの負荷がかかっていたので若干問題意識を持っていた。実際、ググってみると、Ruby での数値演算は (LL の中でも) 遅い傾向にあることが指摘されていたりする。そこで、軽くベンチマークしてみた。

 まず、テスト用に三角関数の演算を1,000万回実行するコードを以下の形で用意した。Ruby の場合、

(1..10000000).each do |i|
  Math.sin(Math.atan2(1, 1) * i / 5)
end

を MRI 1.9.3p194 で実行した。コンパイルにあたっては、-O2 -march=native で最適化している。

 次に比較対象として、最近は若干影が薄くなってきた気もする Perl。実行は同様に最適化した Perl 5.12.4 で実行した。

foreach my $i ( 1 .. 10000000 ) {
  sin(atan2(1, 1) * $i / 5)
}

 そして C。

#include <math.h>

int main (void) {
  int i;

  for (i = 0; i < 10000000; i++) {
    sin(atan2(1, 1) * i / 5);
  }
}

 なお、C については Ruby および Perl をコンパイルした gcc 4.5.3 を用い、最適化を全く行わない状態でコンパイルした。

gcc -lm float.c
strip a.out

 この 3 つを 5 回実行し、その算術平均をとったものが以下になる。

  • Ruby = 12.3948 秒
  • Perl = 2.418 秒
  • C = 0.0262 秒

 実際のアプリケーションをベンチマークする目的でこの処理内容でベンチマークすることにあまり意味はないが、上記のテストコードの場合、Perl は C の 92.3 倍、Ruby は C の 473.1 倍時間がかかっていることになる。また、Ruby と Perl を比べても Ruby は Perl の 5.1 倍かかっている。ちなみに、C のテストコードを Ruby や Perl と同じく最適化した場合、倍の 1 億回計算しても 0.002 秒で終わる。

 以上より、Ruby での数値演算は予想以上に遅いことが判明した。ただし、プロトタイピング用としては、明らかに C よりも Ruby のほうが生産性が高いと思う。今回はこの結果を受けて USB Weather Board のクライアントを Ruby から C で書き直したが、Ruby のコードがなければ 2 〜 3 日で書くのは無理だったと思われる。そういう意味では、当初 Ruby でコーディングしたことは間違いではなかったと思う。

 ちなみに、Ruby では平均風向や日中の最高気温を出すのに SQLite でデータを保存していたが、C 版は自前の原始的なラウンドロビンデータベースを作ってみた。これについてはいずれ書きたい。