謎のベンチマーク

個々の言語で最適化するんじゃなくて、なるべく"同じ"コードで性能を測定。速いコードがほしいなら、Cで書くよって話なので、教科書のアルゴリズムを、そのまま素朴に実装しました的な感じ。

Haskellpythonのようにパラダイムの違う言語間では、同じ書き方をすると、一方が不利になったりするので、C/C++SchemeHaskellはついでで、主にLua/LuaJIT/V8/pythonあたりの差が見たかった。

処理系 空ループ 空末尾再帰 fib(35) mandelbrot集合 挿入ソート tarai
gcc-4.1.2(optionなし) 44ms 205ms 46ms 1ms以下
gcc-4.1.2(-O3) 72ms 30ms 1ms以下
Lua-5.1.4 190ms 1920ms 4300ms 559ms 24390ms 2939ms
LuaJIT 2.0.0 10ms 30ms 379ms 31ms 223ms 280ms
python-2.4 795ms 9768ms 3703ms 48185ms 6756ms
python-2.4+psyco 19ms 444ms 398ms 2472ms 244ms
V8 85ms 498ms 311ms 49ms 340ms
Gauche 0.8.14 1209ms 2551ms 2234ms 1805ms
GHC 6.10.1 941ms <1ms
LuaJIT 2.0.0(jit.off) 66ms 439ms 1242ms 160ms 6000ms 730ms

注釈。
・空ループ(10^7回)
地味に面白い結果になった。
gcc(-O3)に結果がないのは、最適化オプションを付けると、空ループが消えてしまうから。で、LuaJITがgccを上回ってるのは、gccがループ変数をレジスターに割り当ててくれないせい。register修飾子とかつけても、あほなコードを吐くので、asmで書いたら、9msとかになった。多分LuaJITも同じようなコード生成しているのかね。速度的にはそんな感じだけど

pythonは若干謎で、関数の中にループを書かないと倍くらい遅いし、psycoも高速化してくれない。人の書いたpythonコード見てると、if __name__=="__main__"の中にコードを直書きするのではなく、main関数を別に作ってる人がいて、趣味の問題かと思ってたけど、こんな差があるのか。けどまあ、psycoだとV8より速い。

あと、Lua/python/JSのいずれでも、whileで回すより、forで回す方が速い。フーン


・空末尾再帰(10^7回)
空ループを末尾再帰版。gaucheの空ループを、どうしようかと思ったのだけど、なるべく類似性の高いコードで比較するという原則に従って別項目として扱うべきかと思った。しかし、殆どの処理系が再帰限界の前に屈したのだった。だらしない。LuaJITは、なんかイカサマしてるんじゃないかと思うくらいの速さ。


・fib(35)
35番目のFibonacci数。再帰とかどんな感じか的な。やっぱC/C++速い。V8/psyco/LuaJITあたりのネイティブコード野郎たちが、まあ大体同じ速度。GHCはだらしない。ネイティブコードの越えられない壁の後に、バイトコード野郎が続く。Gaucheが意外と速くて、まあでもpythonLuaより遅いねっていう


・mandelbrot集合
よくある80x80のmandelbrot集合の描画。LuaJIT異常に速いな〜っていう。まあ、単純なループの中で、簡単な浮動小数点演算をするだけなので、ループさえ最適化できるなら、C/C++に匹敵できる感じなのかもしれない


・挿入ソート
JSには、ちゃんとした配列があるけど、Luaにはテーブルしかないという差なのかね


・tarai(12,6,0)
GHCに面目躍如させたかった。わけではなく、fib(35)でLuaGaucheに負けてるのが気になったので、再帰をもう一個。やっぱり、gaucheの方が速い



psycoが結構速い。挿入ソートで、V8に大差で負けてるのは、Pythonのリストがindexアクセスが苦手とかいう点が大きい気がする。なので、pyscoはLanguage shootoutで、よい成績を出せそうではある。


それはともかく、そもそも何をしたかったかというと。LuaとかLuaJITが速いとかよく聞くんだけど、なんでなんかな?的な部分がよく分からんというのが動機だった。Lua/JS/pythonはよく似ていて、今回のコードも、{}がbegin~endになるとかの差はあっても、ほぼ同じコードで書けた(まあ、単にC/C++風ともいえる)。pythonなんかは、coroutineなし、再帰限界あり、制限されたlambdaと、機能的にはLuaに大分劣るとさえ言える。なのに、pythonやJSがLuaより遅いとしたら、その原因は、どこらへんにあるのか?(メモリ消費量もなんでそんな少なくて済むのか?というのも)という点が気になった。ていうか、本当に速いの?速いと言っても、何が得意で何が苦手なの?みたいなのを、もうちょっと知りたかった。


で、とりあえず、LuaJITは異常に速い。JITを使用しないですら、通常のLuaの3~4倍の速度。JITなしでもmandelbrot集合の描画とか、V8より速い。JITすれば最適化したC/C++並の速度が出る場合もある。公式のベンチマーク見ると、ここまで速くなったのは、2.0.0かららしい。再帰はループのようにはいかないっぽいけど、Luaはスタックオーバーフローしました!ってわけにはいかないので、そのへんで限界があるような気がしないでもない。


一体なにが起きているのかと思って、ソース見てみたところ、LuaJITは、単に普通のLuaJIT機能を付け加えたものなんだと思ってたけど、(少なくとも、LuaJIT2.0.0では)VMからして全く別物っぽく、LuaJIT VMはLuaVMと異なる命令セットを持って、VMレジスターをCPUのレジスターに割り当てるよう、VMは全部アセンブラで書くという気合の入り方。標準ライブラリもx86で速くなるようにチューニングされているっぽい。VM自体が違うので、実際のところ、何がどのくらい性能向上に貢献しているか分からないし、バイトコードを吐いてくれない/ポータビリティも失われたという点は大きなデメリットではある。


けどまあ、本気出せばここまで速くなるんだな〜というよい目標ではある。尤も、上のベンチマークだと、psycoで10~30倍くらい加速してるけど、実際のアプリケーションでは2~3倍にしか加速しないのが普通だし、Cで書いた部分で時間を食われてれば全く加速しないことも稀に良くあることなので、上のベンチマークに於ける2~3倍くらいの差は、実際のアプリケーションでは誤差程度の差にしかならないかもしれない。Luaはそもそも単体で動かすことはあまり多くないだろうし、LuaJITですらLuaの3~4倍程度だし、果たして、そこまで手間かける価値あるのか?という点は難しいとこであるけど


一方、Luaが速いという点については、どうなんだろう。確かにpythonよりは速そうではある。でも、末尾再帰再帰の速度は、gaucheに負けている。schemeでは再帰が多用される一方、Lua再帰を使うことは比較的少ないだろうので、、不利な勝負ではあるけれど。Language shootout見ても、多くの処理系がJITなりAOTなりでネイティブコード生成するので、それらより遅くても仕方ないという感がある。純粋にバイトコード生成して実行する処理系は、RubyPythonくらい。なので、Luaが速いのではなく、RubyPythonが遅いのでは?という可能性もある。


とはいえ。ループは速いので、何が起きてるか見てみる。

> cat loop.lua
for i=0,10000000 do end
> luac -l loop.lua

main <loop.lua:0,0> (6 instructions, 24 bytes at 0x9302d10)
0+ params, 4 slots, 0 upvalues, 4 locals, 3 constants, 0 functions
        1       [1]     LOADK           0 -1    ; 0
        2       [1]     LOADK           1 -2    ; 10000000
        3       [1]     LOADK           2 -3    ; 1
        4       [1]     FORPREP         0 0     ; to 5
        5       [1]     FORLOOP         0 -1    ; to 5
        6       [1]     RETURN          0 1

なんかfor文専用の命令がある。手続き型言語VMだと、これが普通だったりするのだろうか。for文の中で、何かする時は、FORPREPとFORLOOPの間に色々挟まるらしい。一方、whileで書くと

> cat loop2.lua
local c=0
while 1 do c=c+1 if c==10000000 then break end end
> luac -l loop2.lua

main <loop2.lua:0,0> (7 instructions, 28 bytes at 0x8ab4d10)
0+ params, 2 slots, 0 upvalues, 1 local, 3 constants, 0 functions
        1       [1]     LOADK           0 -1    ; 0
        2       [2]     ADD             0 0 -2  ; - 1
        3       [2]     EQ              0 0 -3  ; - 10000000
        4       [2]     JMP             -3      ; to 2
        5       [2]     JMP             1       ; to 7
        6       [2]     JMP             -5      ; to 2
        7       [2]     RETURN          0 1

これの実行時間は、上のforで書いた時の3倍くらい遅い。ADDして、EQしてJMPするので、大雑把に一命令で済むのが3命令になる感じなのか。Pythonと比較しても、1.5倍くらい速い程度。結局Luaのループが速いのは、単にc++;if(c==10000000)break;という処理を1命令でやってくれるからという部分がでかい。まあ、Luaでループを多用するであろうことを考えれば、合理的な設計ではある。ちなみに、Squirrelを、ちらっと見た感じでは、FORPREP/FORLOOPに相当する命令は見当たらなかった。

ちなみに、cをlocal変数にしないと、無茶苦茶遅くなって、for文の10倍以上遅くなる。バイトコード見ると、SETGLOBAL/GETGLOBALが沢山挟まって、ステップ数が大幅に増える。pythonには、local修飾子とかないので、Luaと比べて不利なのもこの点かな、という気はする。


ついでに末尾再帰

> cat loop3.lua
function loop(n)
  if n<10000000 then
    return loop(n+1)
  else
    return
  end
end

loop(0)
> luac -l loop3.lua

main <loop3.lua:0,0> (6 instructions, 24 bytes at 0x97c2d10)
0+ params, 2 slots, 0 upvalues, 0 locals, 2 constants, 1 function
        1       [7]     CLOSURE         0 0     ; 0x97c2e40
        2       [1]     SETGLOBAL       0 -1    ; loop
        3       [9]     GETGLOBAL       0 -1    ; loop
        4       [9]     LOADK           1 -2    ; 0
        5       [9]     CALL            0 2 1
        6       [9]     RETURN          0 1

function <loop3.lua:1,7> (9 instructions, 36 bytes at 0x97c2e40)
1 param, 3 slots, 0 upvalues, 1 local, 3 constants, 0 functions
        1       [2]     LT              0 0 -1  ; - 10000000
        2       [2]     JMP             5       ; to 8
        3       [3]     GETGLOBAL       1 -2    ; loop
        4       [3]     ADD             2 0 -3  ; - 1
        5       [3]     TAILCALL        1 2 0
        6       [3]     RETURN          1 0
        7       [3]     JMP             1       ; to 9
        8       [5]     RETURN          0 1
        9       [7]     RETURN          0 1

Gaucheの場合

gosh> (define (loop n) (if (< n 10000000) (loop (+ n 1)) '()))
loop
gosh> (disasm loop)
main_code (name=loop, code=0x9fd6ed0, size=11, const=1, stack=4):
args: #f
     0 CONST 10000000
     2 LREF-VAL0-BNLT(0,0) 9    ; (< n 10000000)
     4 LREF0                    ; n
     5 NUMADDI(1)               ; (+ n 1)
     6 PUSH-GREF-TAIL-CALL(1) #<identifier user#loop>; (loop (+ n 1))
     8 RET
     9 CONSTN
    10 RET
#<undef>

Luaと同じような命令が生成される。実行速度も、Luaの1.5倍程度。まあでも、Schemeが、末尾再帰最適化があればループはいらんと言っても、速度的には厳しいものがあるのかな〜という感じではある。とはいえ、LuaJITではJITすると、whileより末尾再帰の方が速くなったりするけど

最後に、Pythonのfor文

>>> def test():
...   for _ in xrange(10000000):pass
...
>>> __import__('dis').dis(test)
  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_CONST               1 (10000000)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (_)
             19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE

もっと速くてもいい気はするけど、謎


色々見てると、Squirrelはどうなんだろうとか、LLVM-luaはどうなんだとか、気になってきたけど、キリがないので、このへんで。


全部貼ると多いので、pythonLuaソースコードだけ。pythonに慣れると、endしかない行とか、}しかない行とかが、とてもうざく感じる

Lua

collectgarbage("stop")

--測定用
function profile(name , f , _args)
  local args={}
  if not (_args==nil) then
    args = _args
  end
  local t = os.clock()
  f(unpack(args))
  t = os.clock()-t
  --速すぎる場合は再測定
  if t<0.1 then
     t=os.clock()
     for i=1,10 do
        f(unpack(args))
     end
     t = (os.clock()-t)/10
  end
  return string.format("%s:%d(msec)" , name , 1000*t)
end

function loop1()
  for i=1,10000000 do end
end

function loop2()
  c=0
  while 1 do c=c+1 if c==10000000 then break end end
end

function loop3(n)
  if n<10000000 then return loop3(n+1) end
end

function fib(n)
    if n<=2 then
       return 1
    else
       return fib(n-1)+fib(n-2)
    end
end


function iterate(x,y)
        local BAILOUT = 16
        local MAX_ITERATIONS = 1000
        local cr = y-0.5
        local ci = x
        local zi = 0.0
        local zr = 0.0
        local i = 0

        for i=1,MAX_ITERATIONS do
                local temp = zr * zi
                local zr2 = zr*zr
                local zi2 = zi*zi
                zr = zr2-zi2+cr
                zi = temp+temp+ci
                if (zi2+zr2 > BAILOUT) then
                        return i
                end

        end
        return 0
end

function mandelbrot()
        local t = os.time()
        for y = -39, 38 do
                for x = -39, 38 do
                        if (iterate(x/40.0, y/40) == 0) then
                                --io.write("*")
                        else
                                --io.write(" ")
                        end
                end
                --io.write("\n")
        end
end


function isort(ls)
  for i = 2, #ls do
    for j = i, 2, -1 do
        if ls[j - 1] <= ls[j] then break end
        ls[j], ls[j - 1] = ls[j - 1], ls[j]
    end
  end
end


ls = {}
for i=1,10000 do
  ls[i] = 10001-i
end


function tarai(x,y,z)
  if x <= y then
     return y
  else
     return tarai(tarai(x-1,y,z),tarai(y-1,z,x),tarai(z-1,x,y))
  end
end


print(profile("空ループ",loop1,{}))
print(profile("空while",loop2,{}))
print(profile("空末尾再帰",loop3,{0}))
print(profile("fib(35)",fib,{35}))
print(profile("mandelbrot",mandelbrot))
print(profile("isort:",isort,{ls}))
print(profile("tarai:",tarai,{12,6,0}))


Python

#-*- coding:utf-8 -*-
import time,gc
gc.disable()

def profile(name , f , args=[]):
   t = time.time()
   apply(f , args)
   t = time.time() - t
   if t<0.1:
      t=time.time()
      for i in xrange(10):
         apply(f , args)
      t=(time.time()-t)/10
   return "%s:%d(msec)" % (name,t*1000)

def loop1():
  for _ in xrange(10000000):pass

def loop2():
  c=0
  while 1:
    c+=1
    if c==10000000:break

def loop3(n):
  if n<10000000:return loop3(n+1)

def fib(n):
  if n<=2:
     return 1
  else:
     return fib(n-1)+fib(n-2)

def iterate_mandel(x , y):
    BAILOUT = 16
    MAX_ITERATIONS = 1000
    cr = y - 0.5
    ci = x
    zi = 0.0
    zr = 0.0
    for i in xrange(MAX_ITERATIONS):
      temp = zr * zi
      zr2 = zr * zr
      zi2 = zi * zi
      zr = zr2 - zi2 + cr
      zi = temp + temp + ci
      if zi2 + zr2 > BAILOUT:
        return i
    return 0

def mandelbrot():
    #stdout = __import__('sys').stdout
    for y in xrange(-39, 39):
      #stdout.write('\n')
      for x in xrange(-39, 39):
        i = iterate_mandel(x/40.0, y/40.0)
        if i == 0:
          #stdout.write('*')
          pass
        else:
          #stdout.write(' ')
          pass


def isort(ls):
    for i in xrange(1, len(ls)):
        for j in xrange(i,0,-1):
            if ls[j]>=ls[j-1]:break
            ls[j-1],ls[j] = ls[j],ls[j-1]

def tarai(x,y,z):
  if x<=y:
    return y
  else:
    return tarai(tarai(x-1,y,z) , tarai(y-1,z,x),tarai(z-1,x,y))

if __name__=="__main__":
   __import__("psyco").full()
   print profile("空ループ",loop1)
   print profile("空while",loop2)
#   print profile("空末尾再帰",loop3)
   print profile("fib(35)",fib,[35])
   print profile("mandelbrot",mandelbrot)
   print profile("挿入ソート",isort,[[10001-i for i in range(10000)]])
   print profile("tarai" , tarai , (12,6,0))