Rubyのクロージャとforとeachと1.8と1.9

Rubyにはクロージャと言うものがある。
ようするに手続きオブジェクトで、Proc.newなどで作ることができる。
こいつのポイントは、それが作られた環境(ローカル変数とか)のスコープと値を保持するところだ。環境を閉じ込めるからクロージャという。らしい。

array = []
(0..2).each do |i|
  array.push(Proc.new {i})
end
p array[0].call # => 0
p array[1].call # => 1
p array[2].call # => 2

array = []
for i in 0..2
  array.push(Proc.new {i})
end
p array[0].call # => 2
p array[1].call # => 2
p array[2].call # => 2

上のコードでは(0..2).eachしているほうは見事にiの値が保持されていることがわかるだろう。
対して下のコードは全部2になっている。
なぜかと言うと、eachはブロック内にローカルのスコープを作るからで、forは作らない(iはforが書かれたスコープ、すなわちグローバルスコープに定義される)からである。
だから上の例でiをforの前で参照すると未定義エラーになるし、forの後で参照すると2が入っている。
クロージャの欠点はこういう細かい挙動が理解できてないとまともに使えないところだ。
MyGameのイベント処理はクロージャを使うが、Webで検索しても活用している人を見かけないのも、たぶんそういう難しさが影響しているのだろう。
っていうか手続きオブジェクトって概念自体が難しい。


ところでeachがローカルのスコープを生成するってことは、実行速度に差が出てくるのではなかろうか。確認してみよう。

require 'benchmark'

GC.start
puts Benchmark.measure {
  for i in 0..10000000
  end
}

GC.start
puts Benchmark.measure {
  (0..10000000).each do |i|
  end
}

GC.start
puts Benchmark.measure {
  (0..10000000).each do
  end
}

GC.start
puts Benchmark.measure {
  i = 0
  while i <= 10000000
    i += 1
  end
}

Ruby1.8.7ruby 1.8.7 (2009-12-24 patchlevel 248) [i386-mswin32])でこれを実行すると、

  2.140000   0.000000   2.140000 (  2.140625)
  3.079000   0.000000   3.079000 (  3.093750)
  1.828000   0.000000   1.828000 (  1.828125)
  7.656000   0.000000   7.656000 (  7.671875)

となる。
一番上はforで、二番目がeachでiをブロックに渡すイテレータだ。確かに速度差がある。
三番目はiを渡さないパターン。forより速い。四番目はwhileだが、余分なメソッドを呼ぶせいでとても遅い。
これをRuby1.9.1(ruby 1.9.1p376 (2009-12-07 revision 26041) [i386-mswin32])で動かすとこうなる。

  4.641000   0.000000   4.641000 (  4.656250)
  3.719000   0.000000   3.719000 (  3.718750)
  3.797000   0.000000   3.797000 (  3.796875)
  3.765000   0.000000   3.765000 (  3.765625)

なんということでしょう。1.8.7より遅い。そしてforが一番遅く、iを渡さない三番目より渡す二番目のほうが速い。全体的に意味がわからん。
VMの命令コードを出力してみると、二番目と三番目はローカルのテーブルを作るかどうかの差しかないから、作ったほうが速くなるというのはちょっと納得が行かない。でもこれが現実。
最後のwhileが1.8.7より大変速くなっているのはVM化の恩恵と言える。メソッドを呼び出さず最適化VM命令のみで実行されているからだ。


結論として、何が言いたいのかよくわからない記事になった。