eval の速度比較

ruby 1.9ruby 1.8 より eval が 3 倍くらい遅いというのは有名 (?) な話です。では、他の LL と比べてどうなんだろうと思ったので、比較をしてみました。

"1" を 100000 回 eval する

eval の前処理と後処理にかかる時間の比較。

ruby 1.8 (trunk) : 0.22 sec
ruby 1.9 (trunk) : 0.82 sec
perl 5.10        : 1.23 sec
python 3.0rc1    : 1.83 sec
php 5.3 alpha 2  : N/A (> 180 sec)

この結果だけみると、ruby 1.9 の eval はそれでも速い方に見えます *1
それはともかく PHP が激遅なんですが、どうも eval を繰り返し呼ぶと、なぜか呼んだ回数以上にどんどん遅くなる (O(n^3) くらい?) ので、意味のある速度比較ができません。ひょっとしたらやんごとなき事情があるのかもしれませんが、きっとバグだと思います。PHP さすがですね!

"1+1+1+ ...(100000 個)... +1" を eval する

すごく深い抽象構文木をパース・評価するのにかかる時間の比較。

ruby 1.8 (trunk) : N/A (SEGV)
ruby 1.9 (trunk) : N/A (SEGV)
perl 5.10        : 0.17 sec
python 3.0rc1    : N/A (SEGV)
php 5.3 alpha 2  : 0.36 sec

ruby もさすがですね! しかし python が落ちると驚きますね (ぼくの中で python は優等生なのです) 。原因はたぶん共通で、深さ 50000 の構文木を関数の再帰呼び出しでたどって、スタックオーバーフローしているものと思われます。
しかし比較にならないなあ。

関数定義 50000 個を eval する

何もしない関数の定義を 50000 個並べたものをパース・評価するのにかかる時間の比較。

ruby 1.8 (trunk) : 1.05 sec
ruby 1.9 (trunk) : 2.81 sec
perl 5.10        : 1.93 sec
python 3.0rc1    : 4.51 sec
php 5.3 alpha 2  : 1.01 sec

想像ですが、このくらいが現実を表しているんじゃないでしょうか。

という感じ?ちゃんと調べてないですけどね。

まとめ

まあ、速度なんてどうでもいい話題な上に、eval の速度なんてかなりどうでもいいですよね。いいんです。

ベンチマークに使ったコード

#!/usr/bin/env ruby19

require "tempfile"

RUBY18 = ["ruby 1.8 (trunk)", "ruby18"]
RUBY19 = ["ruby 1.9 (trunk)", "ruby19"]
PERL   = ["perl 5.10       ", "./local/bin/perl"]
PYTHON = ["python 3.0rc1   ", "./local/bin/python3.0"]
PHP    = ["php 5.3 alpha 2 ", "./local/bin/php"]

def bench(header, *args)
  f = Tempfile.new("bench")
  yield f
  f.close
  time = (1..5).map do
    t = Time.now
    system(*args, f.path)
    (p $?; break []) if $? != 0
    Time.now - t
  end.min
  f.unlink
  puts "%s : %.2f sec" % [header, time] if time
end

type = ARGV[0] || :all

if type == "1" || type == :all

puts "eval \"1\" (100000 times)"

n = 100000
bench(*RUBY18) do |f| f.puts <<SRC
i = 0
while i < #{ n }
  eval("1")
  i += 1
end
SRC
end

bench(*RUBY19) do |f| f.puts <<SRC
i = 0
while i < #{ n }
  eval("1")
  i += 1
end
SRC
end

bench(*PERL) do |f| f.puts <<SRC
for($i = 0; $i < #{ n }; $i++) {
  eval("1")
}
SRC
end

bench(*PYTHON) do |f| f.puts <<SRC
for i in range(#{ n }):
  eval("1")
SRC
end

#bench(*PHP) do |f| f.puts <<SRC
#<?php
#for($i = 0; $i < #{ n }; $i++) {
#  eval("1;");
#}
#SRC
#?>
#end

end

if type == "2" || type == :all

puts "eval 1+1+..(100000 times)..+1"

s = "1+" * 100000 + "1"
bench(*RUBY18) {|f| f.puts 'eval("' + s + '")' }
bench(*RUBY19) {|f| f.puts 'eval("' + s + '")' }
bench(*PERL  ) {|f| f.puts 'eval("' + s + '")' }
bench(*PYTHON) {|f| f.puts 'eval("' + s + '")' }
bench(*PHP   ) {|f| f.puts '<?php eval("' + s + ';"); ?>' }

end

if type == "3" || type == :all

puts "define 50000 functions"

n = 50000
bench(*RUBY18) do |f|
  f.puts "eval <<SRC"
  (1..n).each {|i| f.puts "def foo#{i};end" }
  f.puts "SRC"
end
bench(*RUBY19) do |f|
  f.puts "eval <<SRC"
  (1..n).each {|i| f.puts "def foo#{i};end" }
  f.puts "SRC"
end
bench(*PERL) do |f|
  f.puts "eval(<<SRC)"
  (1..n).each {|i| f.puts "sub foo#{i}() {}" }
  f.puts "SRC"
end
bench(*PYTHON) do |f|
  f.puts "exec(\"\"\""
  (1..n).each {|i| f.puts "def foo#{i}(): pass" }
  f.puts "\"\"\")"
end
bench(*PHP) do |f|
  f.puts "<?php"
  f.puts "eval(\""
  (1..n).each {|i| f.puts "function foo#{i}(){}" }
  f.puts "\");"
  f.puts "?>"
end

end

ついでに、PHP が eval を重ねる事に遅くなっていく様子。

$ ./local/bin/php -v
PHP 5.3.0alpha2 (cli) (built: Oct 31 2008 19:55:08)
Copyright (c) 1997-2008 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2008 Zend Technologies

$ ./local/bin/php -r '
$t = time() + (float)microtime();
while(1) {
  for($i = 0; $i < 10000; $i++) eval("1;");
  $u = time() + (float)microtime();
  print ($u - $t) . "\n";
  $t = $u;
}
'
0.58000898361206
1.6200242042542
3.3400499820709
12.870194911957

*1:それだけ難しいことをしていないという説も?

*2:php の実装は全く知らないで言ってます。勘違いだったら教えてください。

*3:perl 6 はもっと遅くなってるかも?