K.Sasada's Home Page

こめんとのついか

こめんとこめんと!

message

please set comment :).

_10(Mon)

Ruby VM アドベントカレンダーの 10 日目です.

Ruby 2.0 から,ポインタが 64bit である環境では Flonum を導入して Float(浮動小数点数)の計算の高速化を行っています.厳密にいうと,メモリ管理の時間が減っています.

Ruby 1.9 までは,Float の計算結果ごとに,別々のオブジェクトを返していました.

2.times{
  p((1.1 + 1.2).object_id)
}
#=> 5343490
#=> 5343340

計算結果である 2.3 というオブジェクトに対して,それぞれ別々の object_id が与えられていることがわかるかと思います.つまり,別々のオブジェクトになっています.

これは,全ての Float の計算に当てはまりまして,例えば 1.1 + 1.2 + 1.3 ですと,まずは (1.1+1.2) の結果である 2.3 を作りまして,それに 1.3 を足して 3.6 というオブジェクトを返します.つまり,最終的に欲しいオブジェクトは 3.6 だけなのですが,ここでは 2.3 という余計なオブジェクトが作られていることになります.

確認してみましょう(下記では,オブジェクトスペースに,すでに別の Float があるので,1.x ではなく,100.x に変更しています).

x = 100.1 + 100.2 + 100.3
ObjectSpace.each_object(Float){|f|
  p f if f > 100
}
#=>
300.6
200.3
100.3
100.2
100.1
Infinity
1.7976931348623157e+308

余計なオブジェクトである 200.3 が生成されていることがわかります.この例では 1 つだけの余計なオブジェクトですが,a0 + a1 + ... + aN のような N 回の足し算をすると,N-2 個の余計なオブジェクトが作られてしまうことになります.

このように,Float オブジェクトをいちいち作っていると,(1) Float オブジェクト生成のオーバヘッド (2) GC のオーバヘッドが問題になります.とくに,数値計算を頻繁に行っているアプリケーションの場合,基本的に Float オブジェクトは短命であるため,(2) が問題になることが多いです.

では,整数はどうかというと,Fixnum の範囲では余計なオブジェクトは作りません.というか,Fixnum オブジェクトは GC 対象のオブジェクトではありません.確認してみましょう.

n = 1 + 2 + 3
ObjectSpace.each_object(Fixnum){|i|
  p i #=> 何も出力しない
}

これは,Ruby 処理系(MRI)が,Fixnum を特別扱いしているためです(RHG 第2章 オブジェクト を参照).

特別扱いしているので,同じ数値の Fixnum オブジェクトは,同じ object_id を持ちます.先ほどと同じように確認してみましょう.

2.times{
  p((11 + 12).object_id)
}
#=> 47
#=> 47

どちらも 47 になりました.つまり,33 という Fixnum オブジェクトの object_id は 33 ということです(この数値自体は,処理系によって変わることがあります.例えば,64bit でビルドされた Ruby では,67 になります).

また,Fixnum を特別扱いしているので,Fixnum オブジェクトは GC 対象のオブジェクトとしては作成されず,いくら Fixnum オブジェクトを利用しても,GC は起こりません.

GC が起こらないことを確認してみましょう.

puts "GC count (before): #{GC.count}"
10_000_000.times{|i|
  n = i + i
}
puts "GC count (after): #{GC.count}"
#=>
GC count (before): 4
GC count (after): 4

GC 回数が 4 回のまま変わっていないので,この繰り返しを行っても GC が発生していないことがわかります.ちなみについでに,Float がどうなるか,一応確認しておきましょう.

puts "GC count (before): #{GC.count}"
10_000_000.times{|i|
  n = 1.1 + 1.2
}
puts "GC count (after): #{GC.count}"
#=>
ruby 1.9.3p332 (2012-11-15 revision 37660) [i386-mswin32_100]
GC count (before): 1
GC count (after): 776

775 回 GC していることがわかります.

このように,Fixnum は特別扱いされているが,Float は特別扱いされていない,というのが性能を大きく分ける原因でした.

そこで,Float オブジェクトを特別扱いして Float の計算を速くしよう,というのが Flonum です.Fixnum がどう特別扱いされて,どのような性質を持っていたか,というのをまとめると次のようになります.

では,Flonum を実装した処理系で,さらに 64 bit 環境での Float の特長を確認してみましょう.

s = Hash.new(0)
puts "GC count (before): #{GC.count}"
10_000_000.times{|i|
  n = 1.1 + 1.2
  s[n.object_id]+=1
}
puts "GC count (after): #{GC.count}"
p s
ObjectSpace.each_object(Float){|f| p f}
#=>
ruby 2.0.0dev (2013-01-24 trunk 38925) [x86_64-linux]
GC count (before): 4
GC count (after): 4
{5404319552844594=>10000000}
NaN
Infinity
1.7976931348623157e+308
2.2250738585072014e-308

この結果を見ると,

ということがわかり,ほぼ Fixnum と同じ性質を持つことがわかります.

ただ,Fixnum とちょっと違うのが,ObjectSpace で Float のオブジェクトが見えていますね.

NaN
Infinity
1.7976931348623157e+308
2.2250738585072014e-308

が見えています.実は,Flonum では全ての Float ではなく,ある範囲の Float オブジェクトを特別扱いし,その範囲外の Float オブジェクトは従来どおりの,Float オブジェクトを作ります.その特別扱いする範囲とは,+0.0,および,1.72723e-77 より大きく 1.15792e+77 以下の範囲です(細かい数値は違うかも).多分,普通の人はこの範囲を超える値をあまり使わないと思いますので,だいたい Flonum が使われるんじゃないかと思います.

これに伴い,Float,Fixnum ともに freeze されるようになりました.

p 1.frozen?
p 1.2.frozen?
#=>
true
true

さて,では肝心の性能を見てみましょう.

require 'benchmark'
Benchmark.bm{|x|
  x.report{
    10_000_000.times{|i|
      n = 1.1 + 1.2
    }
  }
}

#=>
ruby 1.9.3p385 (2013-02-06 revision 39113) [x86_64-linux]
       user     system      total        real
   1.760000   0.000000   1.760000 (  1.761259)

ruby 2.0.0dev (2013-01-24 trunk 38925) [x86_64-linux]
       user     system      total        real
   1.210000   0.000000   1.210000 (  1.209062)

2.0 のほうが速くなっていることがわかると思います.

では今日はこの辺で.


好きにコメントを編集してください。ただし、あまり他の人のコメントを書き換えることは感心しません。



back

tton 記述が使えます。YukiWikiな記述してりゃ問題ありません。

「行頭に#code」 と、「行頭に#end」 で挟むと、その間の行は pre で囲まれます。プログラムのソースを書くときに使ってください。

例:

#code

(なんかプログラム書く)

#end

リンクは

[[なまえ|http://www.example.org]]

とか

[[http://www.example.org]]

で貼れます。

$Date: 2003/04/28 10:27:51 $