数値解析 in Ruby
仕事である数値解析プログラムを作ることになって、今、仕様の確認のためにプロト版を作っている。
安易な考えでRubyでプロト版を作ることになったのだが、その柔軟性と引き換えにやはり、実行速度が問題になり始めた。
当初の予測では、3次元版でも1時間程度で終わるだろう、と思っていたのだが、実行に6時間(それも要素数をかなり削減して)かかることがわかったので、コレでは、プロト版の命題である実験には使えない。
その一方で、Ruby(と言うか、LL)の柔軟性は捨てがたいので*1、何とかRubyの柔軟性+Cの高速性を実現したいと思っている。
Rubyを高速化する方法は、ボトルネックになっている部分をCに置き換えるしかない。Cに置き換える方法としては、主に二つ。
- RubyInlineモジュールを使って、関数単位でCに置き換え。
- データ構造をRubyオリジナルのArrayから、NArrayなどのNative CのArrayに変える。
このうち、RubyInlineを使う方法は、従来のプログラムをそのままCにRecodingするだけなので、データの受け渡しにさえ気をつければ、非常に修正が簡単。しかしながら、根幹の部分のデータはRubyのデータ構造で残るため、全てを高速化することができない。
NArrayを使用する方法は、行列単位で演算ができるアプリでは、非常に高速な動作が期待できるが、Indexでアクセスしないと行けない場合には、Rubyのループを使わざるを得ないので、Performanceは落ちる。すなわち、高速性を保つためには、Arrayの柔軟性を一部捨てる必要がある。
と言うことで、どれが最適化を試すために、いくつか実験をしてみた。実験内容は下記の通り。
200x200x200のDouble型の配列を用意し(初期値は0.0)、各要素毎に値を代入する。
測定対象は、
- Pure Ruby(配列、ループともにRuby)
- RubyInline(配列はRuby、ループはC)
- NArray(配列はC、ループはRuby)
- NArray+Extension(配列、ループともにC)
である。
実験結果は以下の通り。
速度比 | |
---|---|
Pure Ruby | - |
RubyInline | 1.33 |
NArray | 0.40 |
NArray+Extension | 397 |
『おぉ、圧倒的じゃないか我が軍は』by ギレン総帥
と言うことで、圧倒的にNArrayをC Extensionで展開したバージョンが圧倒的に速い*2。
これで元のプログラムを書き直したらどれぐらい速くなるかなぁ。今から、少し楽しみ。
最後に、実験したときのコードを載っけておく。
- テストベンチ
#!/usr/bin/env ruby -w require 'rubygems' require 'narray' #GC.disable def benchmark leng = 200 test = Test.new(leng) t = Time.now test.create_array() # a.each {|val| puts val} # p a[9][9][9] Time.now - t end
- Pure Rubyのクラス
# Pure Ruby class Test def initialize(leng) @leng = leng @a = Array.new(leng) @a.each_index do |i| @a[i] = Array.new(leng) @a[i].each_index {|j| @a[i][j] = Array.new(leng)} end end def create_array() create_array_main() end def create_array_main() (0 .. @leng-1).each do |i| (0 .. @leng-1).each do |j| (0 .. @leng-1).each do |k| @a[i][j][k] = i * 100 + j * 10 + k end end end end end b1 = benchmark puts "Finish Pure Ruby!"
- RubyInlineのクラス
# RubyInline begin require 'inline' class Test def initialize(leng) @leng = leng @a = Array.new(leng) @a.each_index do |i| @a[i] = Array.new(leng) @a[i].each_index {|j| @a[i][j] = Array.new(leng)} end end def create_array() create_array_main(@a, @leng, @leng, @leng) end inline do |builder| builder.c <<-EOF void create_array_main(VALUE ary, VALUE nx, VALUE ny, VALUE nz) { #define ACC2DARY(var, i, j, k) RARRAY(RARRAY(RARRAY(var)->ptr[i])->ptr[j])->ptr[k] #define GET2DARY(var, i, j, k) NUM2DBL(ACC2DARY(var, i, j, k)) #define SET2DARY(var, i, j, k, val) ACC2DARY(var, i, j, k) = rb_float_new(val) int i, j, k; double temp; for (i = 0; i < NUM2INT(nx); i++) { for (j = 0; j < NUM2INT(ny); j++) { for (k = 0; k < NUM2INT(nz); k++) { SET2DARY(ary, i, j, k, i*j*k); } } } } EOF end end rescue LoadError end b2 = benchmark puts "Finish RubyInline!" p b1/b2
- NArrayのクラス
# Narray class Test def initialize(leng) @leng = leng @a = NArray.float(leng, leng, leng) end def create_array() (0 .. @leng-1).each do |i| (0 .. @leng-1).each do |j| (0 .. @leng-1).each do |k| @a[i, j, k] = i * 100 + j * 10 + k end end end end end b3 = benchmark puts "Finish NArray" p b1/b3
- NArray & Loop Extentionのクラス
# NArray & Extension require 'rubyext' class Test def initialize(leng) @leng = leng @a = NArray.float(leng, leng, leng) end def create_array() RubyExt.set_init_val(@a, @leng, @leng, @leng) end end b4 = benchmark puts "Finish NArray & Extension" p b1/b4
- Ruby ExtensionのCソース
#include "ruby.h" #include "narray.h" #define ARY3(var, i, j, k, nx, ny, val) \ var[((k)*(ny)*(nx)) + ((j)*(nx)) + (i)] = val void set_init_val(double* array, int nx, int ny, int nz) { int i, j, k; for (k = 0; k < nz; k++) { for (j = 0; j < ny; j++) { for (i = 0; i < nx; i++) { ARY3(array, i, j, k, nx, ny, k*100+j*10+i); } } } } void wrap_set_init_val(VALUE self, VALUE na, VALUE nx, VALUE ny, VALUE nz) { VALUE na2; struct NARRAY *n_na; na2 = na_cast_object(na, NA_DFLOAT); GetNArray(na2, n_na); set_init_val((double*)n_na->ptr, NUM2INT(nx), NUM2INT(ny), NUM2INT(nz)); } void Init_rubyext() { VALUE module; rb_require("narray"); module = rb_define_module("RubyExt"); rb_define_module_function(module, "set_init_val", wrap_set_init_val, 4); }
追記('09/10/27)
defineマクロを修正しました。書き方の鉄則をすっかり忘れちゃってるので、前のソースだと思った通りに計算されませんね。
NArrayが1次元配列なので、インデックスでアクセスするため、無理矢理マクロにしてる。もっと、スマートな方法はないのかなぁ?
最後に、参考にさせてもらったサイト。ありがとうございます。