Rubyのcapacityについて調べたのでまとめた。
実際のコードはこちらにまとめた。 https://github.com/ksss/capa
背景
golangのsliceはcapacity(以下capa)を指定できる。
a = make([]int, 0, 1000) // 長さ=0,capa=1000のslice
これをRubyでもできたら便利になる場面があるだろうかと思って調べてみた。
目的
あらかじめメモリーを確保しておくmethodを用意することで、速度的に有利になる場面があるのか調査する。
よい結果が出ればRuby調査用gemとしてまとめる。
実験
準備
ruby
$ ruby --version
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
コード
Rubyに仮想のmethodArray.new_capa
があったとする。
a = Array.new_capa(1000)
実装イメージ
# include "ruby.h"
static VALUE
rb_s_ary_new_capa(VALUE klass, VALUE capa)
{
return rb_ary_new_capa(FIX2LONG(capa));
}
void
Init_ext()
{
rb_define_singleton_method(rb_cArray, "new_capa", rb_s_ary_new_capa, 1);
}
gnuplot
Benchmarkを可視化するためにgnuplotを使用した。
brew install gnuplot
また、gnuplotコマンドラッパー用のgemが便利そうだったので使ってみた。1
gem install gnuplot
結果
n回pushするコードを用意して比べてみた。
初期オブジェクトに0〜10000回、1回刻みでpush回数を変えていった場合のrealtimeとObjectSpace.memsize_of_all
をプロットした。
速度
みにくいが紫色の点がArray.new
、緑の点がArray.new_capa
で初期オブジェクトを作った場合の結果。
メモリー
メモリーは当然ながら顕著な違いが現れた。
通常のArray.new
で作るArrayの初期capaは3である。
そこからpushしていくと容量が足りなくなるのでcapaを増やさなければならない。
しかしながら一づつcapaを増やしてしまうとメモリの再確保回数が多くなってしまうためある程度メモリーを確保してメモリ確保回数を減らしているようだ。
そのため、メモリ確保はどうしても階段状になってしまう。
考察
速度
速度的には今回の計測ではほとんど差はみられないという結果になった。
速度的な性能はreallocによるオーバーヘッドが大きいほど向上し得たとかんがえられるが、その回数があまり多くはないため大した差が産まれなかったものと考えられる。
たとえ差があったとしてもほとんど誤差の範囲なので、このままでは実用的ではないだろう。
メモリー
最初からメモリを確保してしまっているArray.new_capa
ではメモリの再確保は発生していないので最小限の確保だけで済んでいるようだ。
この特性からメモリーリソースがシビアな場面でわずかに威力を発揮することがあるかもしれない。
とはいえArray.new(n)
等で最初から指定した大きさのArrayを作っておき、push
ではなく[]=
をつかうことで、
指定したメモリー量でオブジェクトを作ることは可能なので通常はこちらを使えば問題ないだろう。2
更に調査したいこと
プロットの分散具合
速度グラフをよく見ると、全体的に線形に広がっていっているようにみえる。ほとんどのプロットは下端に集まっているのに対して、上端にも少量のプロットが集まっているように見受けられる。
単純に誤差なのであればコードの実行回数が増えてもまんべんなく広がるように予想されるが、上端に集まるのであれば、特定の条件下で少しだけ性能が劣化するタイミングがあるのかもしれない。
ケース
capaをあらかじめ確保しておいたほうが有利になる場合が他にあるかも知れない。
が、現状思いつかない。
まとめ
結果としてはgolangのようにcapaを指定できる方法がRubyにもあったとしても、ほとんど優位にはならないことがわかった。
したがってこのようなGemを作ることは現状無意味だろう。
というわけでRubyはよくできててすごい。