[ruby-talk:384763] MIDASWAD - Matz is Dumb and so We are Dumb というのを見て笑ってしまった.この方,この情熱をもっと他に傾けたら凄いことができるんじゃないだろうか.
それはともかく,私はこの元になったフレーズ "Matz is nice and so we are nice" (MINASWAN) が,とても嫌いだったりする.
まつもとさんは nice で cool な言語を作った.全くその通り.でも,それと我々は関係ないよねぇ.いや,多分意図はそういう話じゃないと思うんだけど.
こういうのを嫌ってしまう,ひねくれもの(?)だから,なんかバズワード的な,流行を作ったり乗ったりするのが苦手なんだろうな.
Rubinius 2.0pre で,GIL (Giant Interpreter Lock),MRI では GVL (Global Virtual machine Lock) と言い張ってるアレですが,を外して Thread が true concurrent に走るよ,と書いてあったんで,試してみました.
ちなみに,true concurrent って凄いおかしな言葉なので,普通に parallel と言って欲しい.
もう一つちなみに,2.0pre では,Ruby 1.9 の文法も受け付けるそうで,凄いですね.ちゃんと,p(a: :b) みたいなコードも動きました.-X19 とか -X18 とかを起動オプションに付けることで,きちんとそれぞれ対応できるらしい.
で,試したところ,例えば String#<< がスレッドセーフで無いことがわかりました.Rubinius は String#<< などの Ruby のメソッドを,MRI とは違い,Ruby で記述しています.そのため,String#<< などの処理を1つの文字列オブジェクトに同時に行うと,inconsistent な状態になるため,Rubinius の例外が発生してしまうようでした.
$ rbx-2.0.0pre/bin/ruby -X19 -e 'str = ""; (1..1000).map{Thread.new{100.times{str << "."}}}.each{|t| t.join}; p str.size'
An exception occurred evaluating command line code
undefined method `weakref_alive?' on nil:NilClass. (NoMethodError)
Backtrace:
Kernel(NilClass)#weakref_alive? (method_missing) at kernel/delta/kernel.rb:79
{ } in ThreadGroup#prune at kernel/common/thread_group.rb:17
Array#delete_if at kernel/common/array.rb:716
ThreadGroup#prune at kernel/common/thread_group.rb:17
ThreadGroup#add at kernel/common/thread_group.rb:11
Thread#initialize at kernel/bootstrap/thread.rb:152
Thread.new at kernel/bootstrap/thread.rb:95
{ } in Object#__script__ at -e:1
{ } in Enumerable(Range)#collect at kernel/common/enumerable19.rb:145
Range#each at kernel/common/range.rb:164
Enumerable(Range)#map (collect) at kernel/common/enumerable19.rb:145
{ } in Object#__script__ at -e:1
Rubinius::BlockEnvironment#call_on_instance at kernel/common
/block_environment.rb:72
Kernel(Rubinius::Loader)#eval at kernel/common/eval.rb:74
Rubinius::Loader#evals at kernel/loader.rb:559
Rubinius::Loader#main at kernel/loader.rb:735
と思ったら,これは Rubinius の ThreadGroup のバグらしい.1.9 モードで動かすとまずいようです.1.8 モードでは走りきりましたが,MRI とは異なる挙動になります(今度は String#<< ではなくて,Array#<<).
$ time rbx-2.0.0pre/bin/ruby -X18 -e 'o=[]; (1..1000).map{Thread.new{1000.times{o << true}}}.each{|t| t.join}; p o.size'
979087
real 0m3.130s
user 0m1.948s
sys 0m3.508s
ko1@vb:~/build/rubinius$ time ruby-trunk -e 'o=[]; (1..1000).map{Thread.new{1000.times{o << true}}}.each{|t| t.join}; p o.size'
1000000
real 0m0.388s
user 0m0.108s
sys 0m0.100s
異なる挙動,そして,MRI に比べてちょっと遅くなりました.
遅くなるのは,処理系内部でロック処理をしているからだと推測できますが,挙動が異なるのはいいんでしょうか.
開発者の evan に,その辺を聞いてみました.JRuby の開発者の Thomas Enebo も出てきて議論してくれたんですが,結論としては,この辺のロックモデルに関する合意は Ruby 処理系業界(どこ)では出来ていないため,Matz に聞く必要があるね,ということでした.
まつもとさんは,たしか SEGV しなければいいよ派だったので,この挙動も OK という気がしますね.MRI 1.9 的には,互換性の問題から(atomic なポイントが変わるので),恐らく受け入れられないと思うのですが,MRI 2.0 を見据えた議論が必要になるのかもしれません.
MRI では,C メソッド(C で実装されたメソッド)は atomic で動く,ということが(実装の都合から,たまたま)保証されているので,String#<<,Array#<< はきちんと動くわけです.Rubinius(や JRuby)は,これらのメソッドのスレッド安全性(Thread safety)を保証していないため,例えば複数のスレッドからアクセスするような文字列は,正しくロックをしてやる必要があります.
String#<<,Array#<< の atomicity をもって,書きやすい,書きづらい,というのは実はフェアではありません.例えば,foo += 1 というコードは,(1) foo を得る (2) foo + 1 を計算する (3) foo に (2) の結果を代入する,という 3 フェーズになっています.現在の MRI 1.9 の実装では,たまたまこのコードは(foo が Fixnum だった場合は) atomic に実行されるのですが,一般的にこういうコードは同期を加えなければならないわけです.MRI でも,Thread は並列には実行されませんが,並行には実行されるわけですから.
今後,Thread の並列実行を許す,許さない,というのは,難しい問題だと考えます.個人的には,並列実行を許すと,いろいろなところでロックを獲得しなければならず,高速に single (!= parallel) 実行するのを妨げることになるため,あまりやりたくないなぁ,と思っています(あと,やっぱり毎回ロックを考えながら書くのは,書きづらいんじゃないかなぁ...).が,この考え方はもう古いですかねぇ.
というわけで,色々聞いてみたので,久々に Ruby の話を書いてみました.
聞いた話をまとめようと思ったんだけど,なんか自分の考えばっかりになってしまったな.
JRuby が Thread を並列に実行させるのは,実はそんなに大変じゃないんだろうな,などと考えていました.というのは,上記でちょっと書いた性能の話の理由です.
JVM で(細粒度)ロックのオーバヘッドを減らす,というのはアホほど研究があって,いわゆる高速な JVM ってのは,その辺の研究成果を取り入れて,アホほど高速な同期が可能です.というわけで,JVM をベースとしている JRuby では,このようなロックを沢山用いなければならない JRuby は,だからこそ有りなのかな,と思っていました.
さて,Rubinius は,そのような同期オーバヘッドの削減ってのを頑張るのかなぁ,どうなんだろうなぁ,と思っていたところ,並列実行の方向に進む,ということで,実情を見てみた,といったところです.
Rubinius + Mutex だと,現状どれくらい遅いのか試そうと思ってやってみたら,そもそもなぜか簡単なプログラムでデッドロックしてしまった.ガーン.
require 'benchmark'
require 'thread'
tmax = 100
max = 10_000_000 / tmax
Benchmark.bm{|x|
x.report("T/Sh"){
a = []
m = Mutex.new
(1..tmax).map{
Thread.new{
max.times{
m.synchronize{
a.push true
a.pop
}
}
}
}.each{|t|
t.join
}
}
x.report("T/Sh2"){
a = []
(1..tmax).map{
Thread.new{
max.times{
a.push true
a.pop
}
}
}.each{|t|
t.join
}
} if RUBY_ENGINE != 'rbx'
x.report("T/Ex"){
(1..tmax).map{
Thread.new{
a = []
max.times{
a.push true
a.pop
}
}
}.each{|t|
t.join
}
}
x.report("Single"){
a = []
(tmax * max).times{
a.push true
a.pop
}
}
}
こんなプログラムで確認してみようと思ったんだけど,最初のベンチマークでデッドロックっぽい症状になってしまった.
ちなみに,MRI (ruby 1.9.3dev (2011-06-08 trunk 31957) [i686-linux]) だと,以下のように sync を付けると 4 倍遅くなる,って感じです.single が速いのは Thread 生成コストかな.
T/Sh 20.100000 317.020000 337.120000 (289.043957) T/Sh2 5.060000 0.000000 5.060000 ( 5.166369) T/Ex 4.970000 0.000000 4.970000 ( 5.052313) Single 4.640000 0.000000 4.640000 ( 4.649223)
ちなみに Windows では,Mutex が入るととても遅い.というか,1.9.2 がとても遅くて,それを kosaki さんが高速化した,ということだと思う.が,Windows のほうには対応出来ていない,って感じでしょうか.
ruby 1.9.3dev (2011-06-08 trunk 31957) [i386-mswin32_100]
user system total real
T/Sh 47.175000 50.045000 97.220000 ( 72.131159)
T/Sh2 3.339000 0.032000 3.371000 ( 3.413434)
T/Ex 3.151000 0.015000 3.166000 ( 3.183904)
Single 3.244000 0.000000 3.244000 ( 3.274916)
私の感覚だと「オブジェクトを読んで、変更を加える」という操作はデフォルトではunsafeで、safeなものだけそう断りがある、っていうのが自然に思えます。まあモデルに依存するんで、Rubyの文化はわからないですが。
はい.その通りだと思います.Java もそうでしたよね.
JRubyのArray#<<のネタ: http://blog.sarion.jp/nahilog/2010/11/array-is-mt-unsafe-in-jruby.html