Ruby の Thread.pass によるスレッドの明示的切り替えは異常に重い?


404 Blog Not Found:ruby & perl - 軽量プロセスをthreadで代用 にあるスクリプトで遊びながら、ふと、明示的にスレッドを切り替えた(つまり、答えを早く出せる順に並ぶ)場合も見てみたくなって次のようにしたところ、id:sumim:20070513:p1 で軽く触れた、

ちなみに Ruby ではどうか…と試してみようと思ったのですが、(同じことをするのにかかる時間の)桁が4つほど違った

このことの“犯人”が Thread.pass なのだと判明したのでメモ。



使用した async.rb はこんな感じ。小飼さんのスクリプトを簡素化しています。

require 'thread'

ary= [1,5,10,15,20,25].sort!.reverse!
@be_pass = false

def fib(n)
  Thread.pass if @be_pass
  if n < 3 then
    1
  else
    fib(n-2) + fib(n-1)
  end
end

def time_to_run(&blk)
  t0 = Time.now
  blk.call
  Time.now - t0
end

elapsed = time_to_run do
  ary.each do |n|
    print "fib(#{n}) = #{fib(n)}\n"
  end
end
print "Sync ver took #{elapsed} seconds\n\n"

[false, true].each do |b|
  @be_pass = b
  elapsed = time_to_run do
    q = Queue.new
    t = Thread.new { ary.size.times { puts q.shift } }
    ary.each do |n|
      Thread.new { q.push("fib(#{n}) = #{fib(n)}\n") }
    end
    t.join
  end
  print @be_pass ? "Explicit" : "Implicit"
  print "-switching async ver took #{elapsed} seconds\n\n"
end


出力は次の通り。環境は 1.0 Ghz PowerPCOS X

fib(25) = 75025
fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
Sync ver took 1.280229 seconds

fib(10) = 55
fib(15) = 610
fib(5) = 5
fib(1) = 1
fib(20) = 6765
fib(25) = 75025
Implicit-switching async ver took 1.316305 seconds

fib(1) = 1
fib(5) = 5
fib(10) = 55
fib(15) = 610
fib(20) = 6765
fib(25) = 75025
Explicit-switching async ver took 21.583202 seconds


例によって、上のスクリプトSqueak Smalltalk(Squeak3.9)にほぼ直訳して評価した場合、

| array bePass outStream fib elapsed |
array := #(1 5 10 15 20 25) sort reversed.
bePass := {false}.
outStream := String new writeStream.

fib := [:n |
   bePass first ifTrue: [Processor yield].
   n < 3 ifTrue: [
      1
   ] ifFalse: [
      (fib fixTemps copy value: n-2) + (fib fixTemps copy value: n-1)
   ]
].

elapsed := [
   array do: [:n |
      outStream cr; nextPutAll: ('fib({1}) = {2}' format: {n. fib fixTemps copy value: n})
   ]
] timeToRun / 1.0e3.
outStream cr; nextPutAll: ('Sync ver took {1} seconds' format: {elapsed}); cr.

{false. true} do: [:bool |
   bePass at: 1 put: bool.
   elapsed := [
      | queue |
      queue := SharedQueue new.
      array do: [:n |
         [queue nextPut: ('fib({1}) = {2}' format: {n. fib fixTemps copy value: n})] fixTemps fork].
      [array size timesRepeat: [outStream cr; nextPutAll: (queue next)]] forkAndWait
   ] timeToRun / 1.0e3.
   outStream cr; nextPutAll: (bePass first ifTrue: ['Explicit'] ifFalse: ['Implicit']).
   outStream nextPutAll: ('-switching async ver took {1} seconds' format: {elapsed}); cr
].

World findATranscript: nil.
Transcript show: outStream contents


後の両者(明示的にスレッド切り替えをしなかった場合とした場合)の間には、ほとんど差はでないようです。

fib(25) = 75025
fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
Sync ver took 0.597 seconds

fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
fib(25) = 75025
Implicit-switching async ver took 0.574 seconds

fib(1) = 1
fib(5) = 5
fib(10) = 55
fib(15) = 610
fib(20) = 6765
fib(25) = 75025
Explicit-switching async ver took 0.634 seconds'


えーと、念のため。上の Squeak Smalltalkスクリプトにおいて、なぜだか bePass の値が配列の要素になっているとか、ブロックの評価やフォーク(スレッド作成)時に、ひたすら fixTemps とか fixTemps copy していたりするのは、Smalltalk であるにもかかわらず、ブロックがクロージャになっていない Squeak Smalltalk での苦肉の策なのでどうか笑わないでやってください(^_^;)。って、ならばほかの Smalltalk で書けよって話もありますが。w




rubyco さんの最新の記事 rubyco(るびこ)の日記 - JRubyを使ってFizz-Buzz問題を解く で、噂の JRuby が思いのほか簡単に使えそうだということがわかったので、手元の CRuby たちはちょっと古いバージョンですが Ruby 1.8 vs Ruby 1.9 vs JRuby 対決。

$ ruby -v
ruby 1.8.5 (2006-08-25) [powerpc-darwin8.7.0]

$ ruby async.rb
fib(25) = 75025
fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
Sync ver took 0.889433 seconds

fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
fib(20) = 6765
fib(25) = 75025
Implicit-switching async ver took 1.022443 seconds

fib(1) = 1
fib(5) = 5
fib(10) = 55
fib(15) = 610
fib(20) = 6765
fib(25) = 75025
Explicit-switching async ver took 28.118235 seconds
$ ruby1.9 -v
ruby 1.9.0 (2007-03-23 patchlevel 0) [powerpc-darwin8.8.0]

$ ruby1.9 async.rb
fib(25) = 75025
fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
Sync ver took 0.110057 seconds

fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
fib(25) = 75025
Implicit-switching async ver took 0.177561 seconds

fib(1) = 1
fib(5) = 5
fib(10) = 55
fib(15) = 610
fib(20) = 6765
fib(25) = 75025
Explicit-switching async ver took 1.206743 seconds
$ jruby -v
ruby 1.8.5 (2007-05-16 rev 3672) [ppc-jruby1.0.0RC2]

$ jruby async.rb
fib(25) = 75025
fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
Sync ver took 1.192 seconds

fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
fib(25) = 75025
Implicit-switching async ver took 1.106 seconds

fib(1) = 1
fib(5) = 5
fib(10) = 55
fib(15) = 610
fib(20) = 6765
fib(25) = 75025
Explicit-switching async ver took 2.494 seconds


Ruby 1.8 の Thread.pass の遅さの異常性が際だちます。あと、Squeak SmalltalkRuby 1.9 にあっさり負かされているように見えるのが悔しいので、次のなりふり構わない版に替えて対抗を試みます(^_^;)。

Smalltalk at: #BEPASS put: false
Integer compile: 'fib
   BEPASS ifTrue: [Processor yield].
   ^self < 3 ifTrue: [
      1
   ] ifFalse: [
      (self-2) fib + (self-1) fib
   ]'
| array outStream elapsed |
array := #(1 5 10 15 20 25) sort reversed.
BEPASS := false.
outStream := String new writeStream.

elapsed := [
   array do: [:n |
      outStream cr; nextPutAll: ('fib({1}) = {2}' format: {n. n fib})
   ]
] timeToRun / 1.0e3.
outStream cr; nextPutAll: ('Sync ver took {1} seconds' format: {elapsed}); cr.

{false. true} do: [:bool |
   BEPASS := bool.
   elapsed := [
      | queue |
      queue := SharedQueue new.
      array do: [:n |
         [queue nextPut: ('fib({1}) = {2}' format: {n. n fib})] fixTemps fork].
      [array size timesRepeat: [outStream cr; nextPutAll: (queue next)]] forkAndWait
   ] timeToRun / 1.0e3.
   outStream cr; nextPutAll: (BEPASS ifTrue: ['Explicit'] ifFalse: ['Implicit']).
   outStream nextPutAll: ('-switching async ver took {1} seconds' format: {elapsed}); cr
].

World findATranscript: nil.
Transcript show: outStream contents

ブロックで実装していた fib をメソッド「Integer >> #fib」に変更(これで再帰呼び出しが素直に書けます…)。bePass フラグは受け渡し方法を考えるのが面倒だったのでグローバル変数「BEPASS」にしちゃいました。ごめんなさい。


fib(25) = 75025
fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
Sync ver took 0.056 seconds

fib(25) = 75025
fib(20) = 6765
fib(15) = 610
fib(10) = 55
fib(5) = 5
fib(1) = 1
Implicit-switching async ver took 0.068 seconds

fib(1) = 1
fib(5) = 5
fib(10) = 55
fib(15) = 610
fib(20) = 6765
fib(25) = 75025
Explicit-switching async ver took 0.123 seconds