HaskellとF#の解読の練習とちょっとした疑問でRubyに移植してみた

Haskell vs F#
↑の記事とそのトラックバック先の記事では関数型言語が1秒未満の世界で競ってる。なんだか凄い。

でも、これ、そのままRubyで書き直すとどの位になるんだろう。せめてHaskellの3,4倍くらい遅い感じで動くと嬉しいな。勉強にもなるし♪と思って書き直して動かしてみたら桁が違ってあわわとなったメモ。甘ちゃんでごめんなさい。

追記など

他のRuby実装について追記しました。

Cにも移植してみました。
http://d.hatena.ne.jp/takeshinoda/20120208/1328697927

JavaScriptにも移植してみました。
http://d.hatena.ne.jp/takeshinoda/20120211/1328943818

下記にコード置いておきます。
https://gist.github.com/1768665

コードと実行結果

上記エントリのF#版が書き直しやすかったので、まずはそのまま書き直した↓。

改修前版
Node = Struct.new(:df, :branches)

def B(int, double)
  [int, double]
end

def induceBackwardVerySlow(nodes, values)
  n = values.length / 2
  nodes.map do |node|
    node.branches.map do |k, p|
      p * values[n + k] * node.df
    end.inject(&:+)
  end
end

ITERATION = 1000

def last_values(i)
  Array.new(201, i.to_f)
end

def test_tree
  (0..99).map do |i|
    (-i..i).map do |j|
      Node.new(1.0, [B(j - 1, 1.0 / 6.0), B(j, 2.0 / 3.0), B(j + 1, 1.0 / 6.0)])
    end
  end
end
 
value = lambda do |i|
  test_tree.
    reverse.
    inject(last_values(i)) {|values, nodes| induceBackwardVerySlow(nodes, values) }.
    first
end
 
puts (1..ITERATION).map(&value).max

これの実行結果↓

$ ruby -v
ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-darwin11.2.0]
$ time ruby hoge.rb
999.9999999999998
ruby hoge.rb  28.88s user 0.04s system 99% cpu 28.935 total


F#の実行環境は作るの面倒くさいので置いておいて、Haskellのコードを同じマシンで実行してみるとこんな感じ↓。

$ ghc --version                                                                                     
The Glorious Glasgow Haskell Compilation System, version 7.4.1   
$ ghc -O2 hoge.hs
[1 of 1] Compiling Main             ( hoge.hs, hoge.o )
Linking hoge ...
$ time ./hoge 
999.9999999999998
./hoge  0.53s user 0.00s system 99% cpu 0.534 total

54倍差…。

F#のをそのまま移植してるので、そもそも書き方が良くないってのもあると思って工夫してみたけど、ワタクシの才覚ではどうにも計算時間の桁が上がらず。induceBackwardVerySlow()内のinjectやめてそのまま合算したり、Nodeを配列に展開してみたりしたけど、差はたいして埋まらず。アルゴリズム維持したままで差を詰めるのは難しい…(アルゴリズムどう変えれば良いのか分からないけどねw)。

(言い訳がてらに、F#移植してるのになんでHaskellと競ってるねん的なところもありますが…)

そういうモンだと言われてしまうとそうなんだろうけど、こういう書き方良くないよ的なのがあれば教えて欲しいです。もっとオラにRuby力があれば…。

悪あがき↓。27倍差まで縮めた。けど、だいぶ見たくないコードになりました><。

改修後版
def induceBackwardSlow(nodes, values)
  n = values.length / 2
  nodes.map do |node|
    (node[2] * values[n + node[1]] +
     node[4] * values[n + node[3]] +
     node[6] * values[n + node[5]]) * node[0]
  end
end

ITERATION = 1000

def last_values(i)
  Array.new(201, i.to_f)
end

def test_tree
  99.downto(0).map do |i|
    (-i..i).map do |j|
      [1.0, j - 1, 1.0 / 6.0, j, 2.0 / 3.0, j + 1, 1.0 / 6.0]
    end
  end
end

puts((1..ITERATION).map do |i|
  test_tree.
    inject(last_values(i)) {|values, nodes| induceBackwardSlow(nodes, values) }.
    first
end.max)

実行結果↓。

$ time ruby hoge3.rb 
999.9999999999998
ruby hoge3.rb  14.26s user 0.03s system 99% cpu 14.296 total

後者のコードの計算量を減らして(ITERATION=50)プロファイルにかけてみるとこんな感じでした。う〜ん。

$ ruby -r profile hoge3.rb
49.99999999999997
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 53.83    91.49     91.49     5000    18.30    27.59  Array#map
 14.35   115.88     24.39  5000000     0.00     0.00  Array#[]
 12.82   137.67     21.79     5001     4.36    40.25  Range#each
  5.70   147.35      9.68  2000000     0.00     0.00  Float#*
  5.43   156.57      9.22  2000000     0.00     0.00  Fixnum#+
  3.30   162.17      5.60  1500000     0.00     0.00  Float#/
  3.03   167.32      5.15  1000000     0.01     0.01  Float#+
  1.15   169.28      1.96   500000     0.00     0.00  Fixnum#-
  0.13   169.50      0.22     5000     0.04    27.65  Object#induceBackwardSlow
  0.08   169.63      0.13      100     1.30   315.90  Integer#downto
  0.08   169.76      0.13       51     2.55  2712.94  Array#each
  0.05   169.84      0.08     5051     0.02    46.12  Enumerable.map
  0.03   169.89      0.05     5000     0.01     0.01  Fixnum#-@
  0.02   169.93      0.04     5000     0.01     0.01  Fixnum#/
  0.01   169.95      0.02     5000     0.00     0.00  Array#length
  0.00   169.95      0.00        2     0.00     0.00  IO#set_encoding
  0.00   169.95      0.00       50     0.00     0.00  Object#last_values
  0.00   169.95      0.00       50     0.00     0.00  Class#new
  0.00   169.95      0.00       50     0.00     0.00  Array#initialize
  0.00   169.95      0.00       50     0.00     0.00  Fixnum#to_f
  0.00   169.95      0.00       50     0.00   631.80  Object#test_tree
  0.00   169.95      0.00       50     0.00   631.80  Enumerator#each
  0.00   169.95      0.00        3     0.00     0.00  Module#method_added
  0.00   169.95      0.00       50     0.00  2767.20  Enumerable.inject
  0.00   169.95      0.00       50     0.00     0.00  Array#first
  0.00   169.95      0.00       49     0.00     0.00  Float#<=>
  0.00   169.95      0.00        1     0.00     0.00  Enumerable.max
  0.00   169.95      0.00        1     0.00     0.00  Float#to_s
  0.00   169.95      0.00        2     0.00     0.00  IO#write
  0.00   169.95      0.00        1     0.00     0.00  IO#puts
  0.00   169.95      0.00        1     0.00     0.00  Kernel.puts
  0.00   169.95      0.00        1     0.00 169950.00  #toplevel
改修前と後の差分
$ diff hoge.rb hoge3.rb
1d0
< Node = Struct.new(:df, :branches)
3,7c2
< def B(int, double)
<   [int, double]
< end
<
< def induceBackwardVerySlow(nodes, values)
    • -
> def induceBackwardSlow(nodes, values) 10,12c5,7 < node.branches.map do |k, p| < p * values[n + k] * node.df < end.inject(&:+)
    • -
> (node[2] * values[n + node[1]] + > node[4] * values[n + node[3]] + > node[6] * values[n + node[5]]) * node[0] 23c18 < (0..99).map do |i|
    • -
> 99.downto(0).map do |i| 25c20 < Node.new(1.0, [B(j - 1, 1.0 / 6.0), B(j, 2.0 / 3.0), B(j + 1, 1.0 / 6.0)])
    • -
> [1.0, j - 1, 1.0 / 6.0, j, 2.0 / 3.0, j + 1, 1.0 / 6.0] 32c27 < reverse.inject(last_values(i)) {|values, nodes| induceBackwardVerySlow(nodes, values) }.
    • -
> inject(last_values(i)) {|values, nodes| induceBackwardSlow(nodes, values) }.

【追記】他のRuby実装でやったらめっさ速かったり遅くなったりする

JRuby (改修後版で実行)
$ ruby -v
jruby 1.6.5.1 (ruby-1.8.7-p330) (2011-12-27 1bf37c2) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_29) [darwin-x86_64-java]
$ time ruby hoge3.rb
1000.0
ruby hoge3.rb  10.04s user 0.28s system 133% cpu 7.724 total

おおっ。14倍。浮動小数点の出力の仕様が少し違うらしく、丸まっている。

JRuby(改修前版で実行)
$ time ruby hoge.rb
1000.0
ruby hoge.rb  62.71s user 1.04s system 204% cpu 31.139 total

58倍。ありゃ。だいぶ遅い。

MacRuby(改修後版で実行)
$ ruby -v
MacRuby 0.10 (ruby 1.9.2) [universal-darwin10.0, x86_64]
$ time ruby hoge3.rb
999.99999999991
ruby hoge3.rb  15.73s user 0.45s system 125% cpu 12.855 total

おお、24倍。

MacRuby(改修前版で実行)
$ time ruby hoge.rb
999.99999999991
ruby hoge.rb  250.99s user 7.28s system 146% cpu 2:56.73 total

分越えちゃったよ。330倍。かかりすぎ><

@watson1978 さんにGCDという便利なライブラリを教えて貰った

今回はあえてマルチスレッド化はしていなかったんだけど、すばらしいライブラリですね。最近はいろんな言語にmap的な処理を自動で並列化してくれるライブラリの存在を聞くので、Rubyにもこういったのが出てきてるのは嬉しい。

軽いまとめ

改修版はベンチマーク向きのコードなんだろうね。攻守共に優れてるのがYARVなのかな。