Erlang の ring benchmark を Squeak Smalltalk で


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 秒強だというなら、まあわるくないかな…と。