與 ko1 的一些聊天內容
edited 2012-12-14 00:21
忘記提關於 fibers 的事,見:
2224. 12-14 與 ko1 的一些聊天內容 (2)
抱歉拖稿了好幾天。其實我原本只是想問一些對於 llvm 與 rubinius 的看法而已,
不過總之後來在丹堤聊了約一個小時。事情的經過等等在講 rubyconf.tw/2012 的
心得時再說。
聊天的內容大抵上圍繞著 ko1 之後想做的事,也可以說是 cruby 的未來吧。我得
承認說,聽完這些後,對 cruby 又有了不少希望 (?) 不過說是這樣說,這些幾乎
每一個都是很龐大的計畫,難度也都很高,先別說可能要很久才能完成,能不能完成
都很難說。
以下的訊息除了從 ko1 口中說出外,為了讓行文流暢,也包含我個人的想法。我沒有
寫任何筆記,全部靠大腦回憶三、四天前的事,全文不代表 ko1 的發言 XD
*
從 GIL/GVL 開始說好了。大家知道 cruby 一直不肯拿掉 GIL/GVL, 真實理由是什麼?
這很難一言以蔽之,有好幾個理由。我後來也想起來,我記得有看過討論,ko1 幾年前
曾經把 GIL/GVL 拿掉過。ko1 說好像是五年前的樣子,如果要跟現在的程式合併,
還需要很大的力氣。但總之結果不是很令人滿意,跑單 thread 程式速度下降,程式變得
難寫,內部結構變得複雜,等等。
jruby 的作法是 fine grained locking, 即在真正需要 lock 的地方 lock.
似乎很多 jvm based language 是這樣實作的。ko1 說,這是由於 jvm 的 jit
系統很厲害[0],如果說程式本身是 single threaded, 那麼 synchronize 的
block 是可以被 optimize 掉的。因此 jruby 這樣做的成本相較之下沒有 cruby
來得高。同時,要做到像 jvm 這樣的能力是很困難的(ko1 笑 XD)
同時 ko1 舉了一個例子,想說明 jruby 的作法其實像是 c++ 的作法,即 libraries
本身不是 thread-safe, 如果使用者希望是 thread-safe 的操作,需要自己 lock.
我們知道 c++ 的 STL 基本上都是 thread-unsafe 的,看來 jruby 的 string
亦若是。例子是這個:
size = 10000000
s = ''
t0 = Thread.new{
size.times{ s << 'x' }
}
t1 = Thread.new{
size.times{ s << 'y' }
}
t0.join
t1.join
在 cruby 下,程式不會有問題。在 jruby 上,在我的電腦上,這程式會爆炸,
而且爆炸的原因是 java exception. 我不知道 jruby 本身能不能 rescue
java exception, 合理推測是不能,同時如果不能的話,這跟 cruby segfault
又有什麼不一樣?[1]
jruby 1.7.1 (1.9.3p327) 2012-12-03 30a153b on Java HotSpot(TM) 64-Bit Server VM 1.7.0_09-b05 [darwin-x86_64] Exception in thread "RubyThread-1: crash.rb:1" java.lang.ArrayIndexOutOfBoundsException at org.jruby.util.ByteList.ensure(ByteList.java:342) at org.jruby.RubyString.modify(RubyString.java:923) at org.jruby.RubyString.cat(RubyString.java:1359) at org.jruby.RubyString.cat19(RubyString.java:1324) at org.jruby.RubyString.cat19(RubyString.java:1317) at org.jruby.RubyString.append19(RubyString.java:2580) at org.jruby.RubyString.concat19(RubyString.java:2611) [...]
另一個有趣的是,後來跟 fumin 驗證 s 的輸出會是什麼。看起來 cruby 會輸出
像是 xxxxxxyyyyyy 的東西。同時那個 xxxxx 會很長很長,可能是 1000000
那麼長。合理推測這是為了加速 GIL/GVL 下的影響,實際上 YARV 的執行狀況
可能是透過時間去切割哪個 thread 可以取得執行權。
實際運作這段程式時,確實 cruby 是吃滿一顆核心。
接著跑 rubinius, 跑出來的結果則是非常漂亮的 xyxyxyxyxy, 同時吃滿兩顆核心。
因此我想 rubinius 的 scheduling 應該會是相當 "fair" 的。也理所當然地,
rubinius 的執行速度比 cruby 要來得快。不過也只稍微快一點點而已,跟理想上的
「兩倍快」有很大的距離。我的電腦上大約是 1.2 倍快。這可能也意味著 cruby 在跑
single threaded 的程式時,還是無人能敵的。
ko1 也不知道 rubinius 是不是用 fine grained locking. 我也沒看到那麼細,
所以也不清楚。
*
取而代之的是,ko1 與 matz 想走 multi-processes 的路線。ko1 認為 thread
safe 的程式太過難寫,process 會是比較好利用多核心的方式。[2] 但是理所當然,
如果是 multi-processes 就會面臨到使用太多記憶體的問題。因此他們想了很多方法,
希望能改善這個問題。
首先最顯而易見的是,ko1 希望能把 ruby AOT compile 至 binary code.[3] 接著
則是我不太懂的地方。他列出各種 binary object code, 例如在 linux 上是 ELF,
還有幾個我忘記了。我不確定他的意思是不是這樣,但如果我沒誤解,也辦得到的話,那
ko1 希望能發明另一種 container 可以存放 ruby binary code, 並放到 .so 檔裡面,
使得不同的 process 可以共享同一份 ruby standard library!
同時 compile 成 .so 也需要包含 packaging 的功能,甚至是 encryption 和
obfuscation 的功能。ko1 提到 python 有 package 的功能,但似乎沒有什麼東西
有做到 encryption 和 obfuscation. (java? ko1 沒有特別回答) 這些東西很
顯然是「大公司」會想要的。雖然不會是我想注意的目標,但確實可以讓大公司感興趣的話,
還是有很多好處的。瞧瞧 jvm?
下一步則是 MVM (Multiple VM), 使得多 process 的管理能夠更為方便。但是實作
這個的困難點在於,ko1 說由於 cruby 是個跑了 20 年的 project, 裡面用了很多
比較老式的架構,包含了許多的 global state, 因此要達成 multiple vm 不是那麼
容易,可能需要改很多很多的程式才有辦法。
mruby 則是用了相對很新的架構,要達成 multiple vm 要來得容易許多。這時我提到了
希望可以擊敗 lua.... XD
接下來則是 process 與 process 之間溝通的問題。ko1 說他還在大學教書時,有個
學生做了一個跑在 linux 上的 process 跟 process 之間溝通的 protocol. 這個
細節我不太記得了,因為我跟 linux 沒那麼熟,不知道 linux 有哪些 sharing
memory 的方法。不過大抵上就是我們會希望可以做到像是 ruby primitive 的物件,
例如數字或是字串的東西,可以用另外一種 marshaling 的格式。例如與其真的把
ruby string marshal 之後傳過去,不如直接用個 tag 表達這是 ruby string,
接著把真正的 bytes 傳過去即可。
這樣一來,至少 primitive type 的傳遞會是高效能的。那學生的實驗是在 24 核心的
電腦上完成的,很貴很貴 XD
這或許也可以讓 distributed object 變得容易。不過 ko1 說透過網路的話就很慢了。
但我想當然網路會很慢,可是如果我們需要跨機器的溝通,嗯,或許是像之前聽到的,
有種特殊的 protocol 可以讓機器與機器間更高速溝通。或許真的用什麼來溝通才是重點,
而非溝通什麼吧。
*
ko1 也提到 IBM 有個人用了 HTM (hardware transactional memory) 做了 ruby
相關的東西?還沒有機會交流,所以要找機會聊聊。我記著我接下來就說也希望 ruby 可以
提供 STM (software transactional memory) 的支援。
ko1 說問題在於有些東西是沒辦法 rollback 的。這時我就說可以像 haskell 一樣區分
I/O operations, 可惜沒聽到什麼回應 XD
另外我也提了之前看到 pypy 有用 STM 拿掉 GIL/GVL, 由於那是僅限於內部使用的
STM, 因此都是在控制的環境下使用,比較不會有 I/O 無法 rollback 的問題,或是
如果碰上 I/O 的東西,還是可以用一般的方式做 locking.
連結在這:
We need Software Transactional Memory
STM update: back to threads?
STM with threads
可惜也是沒聽到什麼回應。
*
接著是關於 rubinius 的事。其實這是最早說的,不過可能比較不重要,所以放在最後說。
ko1 提到一點他不喜歡 rubinius 的狀況,就是由於很多內部的東西是由 ruby 寫成的,
單跑 ruby -e raise 我們會看到:
-e:1:in `': unhandled exception
但是單跑 rbx -e raise 會看到什麼?
An exception occurred evaluating command line code No current exception (RuntimeError) Backtrace: { } in Object#__script__ at -e:1 Rubinius::BlockEnvironment#call_on_instance at kernel/common /block_environment.rb:75 Kernel(Rubinius::Loader)#eval at kernel/common/eval.rb:75 Rubinius::Loader#evals at kernel/loader.rb:584 Rubinius::Loader#main at kernel/loader.rb:815
我同意對於不想探究細節的人而言,這些其實都是噪音。因此這應該是一種 trade off,
我是覺得可以看到 raise 是怎麼實作出來的,不是什麼壞事。同時一個可能的解決辦法是,
對印出 backtrace 的地方動手腳,把 kernel/* 的部份全部去掉。啊其實就是 rails
會幹的事情啦~~~
另一個關於 rubinius 或 jruby 的最佳化的問題,ko1 說 method inlining
也許不是那麼有利的事情。在 micro benchmark 中,method inlining 當然很有利,
因為需要最佳化的瓶頸非常明確。
loop do lambda{}.call end
類似這樣的東西,也就是 micro benchmark 裡可能會出現的沒意義的例子,最佳化
這個當然很有利。可是在大型程式裡面,hot spot 可能是任何的 code, 甚至路徑會
不斷改變,使得 hot spot 的位置不斷變動。
在這種情況下,method inlining 就不是那麼有利。這也是為什麼 luajit 的
tracing jit 會在某些情況下放棄 jit, 或是在某些情況下才會開始 jit. 我想分析這個
將會是最大的困難,也是影響最舉足輕重的地方。
由於太難做了,完全不做,有時候反而會有利許多。我猜這可能也是為什麼我的一些測試
還是 cruby 跑最快,其他的有的會慢到很可怕的境界。不過老實講,我也很久沒試了,
說不定最近又有很大的改變。
*
最後最後是關於 llvm 的事。ko1 說幾年前試 llvm 2.6 時,跟現在 api 完全不同了 XD
確實 llvm api 的變化非常大,要一直跟隨他們腳步不太容易。但 ko1 也說現在日本也有
很多 llvm 的資源可以看,也可能 api 已經穩定了,因此說不定是時候進場了。
*
以上就是我目前還記得的內容。
--
[0] jit 是我說的,ko1 先說 no, 然後想了一下,又說 yes, yes, 所以
我猜應該是 jit 沒錯吧?
[1] ko1 先提到別人討厭 cruby segfault, 接著我附和在 jruby 上有
java exception 基本上跟 cruby segfault 的意思差不多 XD
[2] """it's my opinion""" :D
[3] 正好我之前才跟 ko1 說幾年前我想用 RubyVM::InstructionSequence 做出
AOT compilation, 但是失敗。ko1 後來那段英文我聽不太懂... 但我隱隱約約猜
他的意思是 InstructionSequence compile 出來的 byte code 不是完整的
byte code, 實際要執行需要一些 read/write barrier 的 code. 他解釋了不少,
不過真抱歉我聽不太懂... 可能需要電腦寫一下單字 :P
0 retries:
Post a Comment
Note: Only a member of this blog may post a comment.