haskell - haskellのある暮らし - リングノードベンチマーク: Erlang vs Haskell 経由で知った、Programming Erlang の Chapter 7 [β版 PDF] にある課題に Squeak Smalltalk にて挑戦。
そのまえに、Squeak Smalltalk でどのていど太刀打ちできるものなのか感触を得るため、くだんの章の中程にある、Erlang のプロセス(スレッド)がいかに軽量かを示してみせるコード (processes.erl) に類似したものを Squeak Smalltalk で試してみます。まず、手元の環境(1.2 Ghz PenM, WinXP)で Erlang の場合。
$ erl +P 300000 Eshell V5.5.4 (abort with ^G) 1> c(processes). {ok,processes} 2> processes:max(1000). Maximum allowed processes:300000 Process spawn time=0.00000e+0 (0.00000e+0) microseconds ok 3> processes:max(5000). Maximum allowed processes:300000 Process spawn time=3.20000 (6.20000) microseconds ok 4> processes:max(10000). Maximum allowed processes:300000 Process spawn time=3.10000 (6.30000) microseconds ok 5> processes:max(50000). Maximum allowed processes:300000 Process spawn time=5.62000 (6.88000) microseconds ok 6> processes:max(100000). Maximum allowed processes:300000 Process spawn time=3.44000 (4.21000) microseconds ok 7> processes:max(200000). Maximum allowed processes:300000 Process spawn time=3.05000 (4.37500) microseconds ok 8>
30 万プロセス超でおかしくなりますが(なのでここではリミットを 30 万までにしてあります)、直前まで1プロセス生成あたりにかかる時間はびくともしません。すごいですね。
Squeak Smalltalk では、ブロックオブジェクトに対するメッセージ「fork」の送信でプロセス(スレッド)の生成ができます(余談ですが、ある意味、Smalltalk よりずっとアラン・ケイのメッセージングのオブジェクト指向を実践できるであろう Erlang を前にして「メッセージを送信」とか言うのは、ちょっと気が引けますね…(^_^;))。これをふまえて、processes.erl と似たようなことをさせるのに、次のように書いてみました。
| numProcess processes time | (1 to: 9), (10 to: 60 by: 10) do: [:nn | Smalltalk garbageCollect. numProcess := nn * 1e3. time := [processes := (1 to: numProcess) collect: [:idx | [[Processor yield] repeat] fork]] timeToRun. processes do: [:proc | proc terminate]. World findATranscript: nil. Transcript cr; show: ('N = {1}, {2} microseconds per process' format: {numProcess. time * 1.0e3 / numProcess roundTo: 0.1})]
トランスクリプトへの出力結果はこちら。
N = 1000, 4.0 microseconds per process N = 2000, 4.5 microseconds per process N = 3000, 4.3 microseconds per process N = 4000, 4.8 microseconds per process N = 5000, 4.4 microseconds per process N = 6000, 4.7 microseconds per process N = 7000, 6.7 microseconds per process N = 8000, 4.9 microseconds per process N = 9000, 4.8 microseconds per process N = 10000, 4.8 microseconds per process N = 20000, 5.4 microseconds per process N = 30000, 32.9 microseconds per process N = 40000, 64.7 microseconds per process N = 50000, 83.7 microseconds per process N = 60000, 92.8 microseconds per process
2 万プロセスくらいまでは予想外に健闘しています。そのあとだんだん重くなって 6 万プロセス超で戻ってこなくなります。ちなみに Ruby ではどうか…と試してみようと思ったのですが、桁が4つほど違ったのでやめにしておきました。
さて、本題のリング状に連ねたプロセス(スレッド)間でメッセージを指定回数、流す実験。Erlang ではこんなふうに書いてみました。
-module(ring_bench). -export([start/2, make_ring/3]). start(N,M) -> statistics(runtime), statistics(wall_clock), First = spawn(?MODULE, make_ring, [N-1, M, self()]), receive {Last} -> Last ! {First} end, First ! message, receive {Last} -> ok end, {_,Time1} = statistics(runtime), {_,Time2} = statistics(wall_clock), .io:format("N = ~p, M = ~p; elapsed time = ~p (~p) miliseconds~n", [N, M, Time1, Time2]). make_ring(0,M,Top) -> Top ! {self()}, receive {Neighbor} -> ok end, loop(Neighbor, M), Top ! {self()}; make_ring(N,M,Top) -> Neighbor = spawn(?MODULE, make_ring, [N-1, M, Top]), loop(Neighbor, M). loop(_,0) -> ok; loop(Neighbor,M) -> % .io:format("Pid = ~p, M = ~p~n", [self(), M]), receive message -> Neighbor ! message end, loop(Neighbor, M-1).
結果は次の通り。
$ erl Eshell V5.5.4 (abort with ^G) 1> c(ring_bench). ./ring_bench.erl:12: Warning: variable 'Last' exported from 'receive' (line 8) {ok,ring_bench} 2> ring_bench:start(100,1000). N = 100, M = 1000; elapsed time = 62 (62) miliseconds ok 3> ring_bench:start(1000,1000). N = 1000, M = 1000; elapsed time = 438 (469) miliseconds ok 4> ring_bench:start(10000,1000). N = 10000, M = 1000; elapsed time = 7234 (7547) miliseconds ok
恐るべし。
似たようなことを Squeak Smalltalk で次のように書いてみました。なお、Squeak Smalltalk のプロセス(スレッド)…に限らずオブジェクトは、非同期通信ができないので、メールボックスを別に用意して、それをプロセス(スレッド)に適宜チェックさせることで Erlang のプロセス間通信に代えてあります。
| elapsedTime | {1}, (5 to: 20 by: 5) do: [:nn | Smalltalk garbageCollect. elapsedTime := [ | numNodes numMsgs firstMailbox neighborMailbox | numNodes := nn * 100. numMsgs := 1000. firstMailbox := neighborMailbox := OrderedCollection new. (1 to: numNodes) do: [:idx | | myMailbox semaphore | myMailbox := neighborMailbox. neighborMailbox := (idx = numNodes) not ifTrue: [OrderedCollection new] ifFalse: [firstMailbox]. semaphore := Semaphore new. [ | mutex numUnsent | mutex := Semaphore forMutualExclusion. numUnsent := numMsgs. [numUnsent > 0] whileTrue: [ mutex critical: [ myMailbox ifNotEmpty: [ "Transcript cr; show: ('Pid = {1}, M = {2}' format: {Processor activeProcess name. numUnsent})." numUnsent := numUnsent - 1. myMailbox removeFirst. neighborMailbox add: #message]]. Processor yield]. (idx = numNodes) ifTrue: [semaphore signal]] fixTemps fork]. firstMailbox add: #message. semaphore wait] timeToRun. World findATranscript: nil. Transcript cr; show: ('N = {1}, M = {2}; elapsed time = {3} milliseconds' format: {numNodes. numMsgs. elapsedTime})]
Erlang に比べて醜いことと言ったら…。orz で、トランスクリプトへの出力結果。
N = 100, M = 1000; elapsed time = 714 milliseconds N = 500, M = 1000; elapsed time = 3711 milliseconds N = 1000, M = 1000; elapsed time = 7369 milliseconds N = 1500, M = 1000; elapsed time = 12337 milliseconds N = 2000, M = 1000; elapsed time = 16892 milliseconds
もちろん Erlang には遠く及びませんが、手元では jmk さんの Haskell 版の ring 1000 1000 が 30 秒弱だったので、それが 7 秒強だというなら、まあわるくないかな…と。