Vectorと破壊的メソッド

DXRuby1.1もDXRubyFramework0.0.3も、VectorクラスはImmutableとして作っている。
もともとMatrixは3Dのビューやらの設定用、VectorはShaderのパラメータ用、みたいな感じで作ってあったから、特にShaderの設定値はVALUEでそのまま保持する都合上Immutableじゃないと面倒だった、というのが理由だ。
しかしよく考えてみるとDXRubyFrameworkではShaderは関係ないのでMutableでも全然問題ない。
ちなみにShaderだって設定値をコピーするようにすれば何も問題はない。


Mutable(変更可)とImmutable(変更不可)というのはRubyで言うなら自己破壊できるかどうか、という意味に捉えておけばよい。
例えばFixnumやFloatはImmutableだし、StringやArrayはMutableだ。
変数に突っ込んだオブジェクトが、それ自身は同じオブジェクトなのに保持する値が変化するのがMutable。
そのオブジェクトであれば絶対に値が変わらないのがImmutable。


で、話は戻ってDXRubyFrameworkのVector。最初に書いたようにImmutableにしてある。
思想的にはVectorはそれそのものが一つの値を表し、値の入れ物というわけでは無い。だからImmutable。と言う理由でそうしているわけではない。最初に書いた通りだ。でも、そういう考えを持つ人もいる。
プログラミング的にはだいたいにおいてArrayを応用してVectorを作るイメージになるだろうから、元の特性を引き継いでMutable。それはそれで問題ない。NArrayなんかまさにNArrayのサブクラスとしてVectorを作っている。


じゃあどっちがいいのかな、と言った時にはとりあえずコードとベンチ。
DXRubyFrameworkのVectorに自己破壊メソッドVector#addを追加してみた。作るのは簡単。

require 'dxruby'
require 'dxrubyfw'
require 'benchmark'

class Hoge
  attr_accessor :x, :y, :v
  def initialize
    @x = 0.0
    @y = 0.0
    @v = Vector.new(0.0, 0.0)
  end
end

Benchmark.bmbm { |bm|
  bm.report("xy              ") {
    a = Hoge.new
    1000000.times {
      a.x += 1.0
      a.y += 1.0
    }
  }
  bm.report("vector_immutable") {
    a = Hoge.new
    vec = Vector.new(1.0, 1.0)
    1000000.times {
      a.v += vec
    }
  }
  bm.report("vector_mutable  ") {
    a = Hoge.new
    1000000.times {
      a.v.add(1.0, 1.0)
    }
  }
}

3パターン。
Spriteのようなオブジェクトに対して、座標x/yをそれぞれ1.0足す。右下に100万ほど移動させるようなイメージだ。
xyはそれぞれに1足す。Floatオブジェクトが毎回2個生成される。
vector_immutableは足し算用のVectorオブジェクトを作っておいてそれを+メソッドで足す。+メソッドは新たなVectorを生成するから、その負荷がある。
vector_mutableはaddメソッドで値を直接足す。addメソッドは自己破壊型で自分自身の値を変化させる。
結果は以下。ruby 1.9.1p378 (2010-01-10 revision 26273) [i386-mingw32]。

Rehearsal ----------------------------------------------------
xy                 0.609000   0.000000   0.609000 (  0.000000)
vector_immutable   0.641000   0.015000   0.656000 (  0.000000)
vector_mutable     0.219000   0.000000   0.219000 (  0.000000)
------------------------------------------- total: 1.484000sec

                       user     system      total        real
xy                 0.516000   0.000000   0.516000 (  0.000000)
vector_immutable   0.640000   0.047000   0.687000 (  0.000000)
vector_mutable     0.203000   0.000000   0.203000 (  0.000000)

xyはしょっぱなGCの影響を受けるのでbmbm。まあ、たくさんオブジェクト生成するし。
これを見るとFloatの生成はVectorの生成よりずっと速いらしい。VectorにはCの構造体を確保するためのmallocもあるし、RubyVMによるFloat#+の最適化も大きな要因だろう。
MutableのVectorはさすがに速い。座標計算が主たる負荷であればこれは大きな差になるだろうが、それだけ複雑・大量演算をするのならこの書き方では辛い。四則演算のメソッドを使ったらどのみにオブジェクトが生成される。なかなか難しいところだ。


DXRubyFrameworkのVectorについては、このままImmutableにしておく利点も特に思いつかないのでしばらく悩んでみる。