数値解析 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)、各要素毎に値を代入する。

測定対象は、

  1. Pure Ruby(配列、ループともにRuby)
  2. RubyInline(配列はRuby、ループはC)
  3. NArray(配列はC、ループはRuby)
  4. 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次元配列なので、インデックスでアクセスするため、無理矢理マクロにしてる。もっと、スマートな方法はないのかなぁ?


最後に、参考にさせてもらったサイト。ありがとうございます。

*1:入力ファイルとかしょっちゅう変わるだろうから

*2:ま、当たり前だけどね