Squeak Smalltalkを多コア対応させる10年程前の試み「RoarVM」で再び遊ぶ

昨年末の登場以来、Apple の M1マシンの爆速ぶりが大々的にフィーチャーされ続けているおかげもあってか、インテル版のそこそこ盛った仕様の MacBook Pro(16インチ, Late 2019)の中古価格にかなりお手頃感が出てきました。そこで、いろいろ不具合が蓄積してきていた第4世代 Core i7 搭載の MacBook Air(Early 2014)の代替として入手してみました。M1 ほどではないにしても 8コア16スレッドの第9世代 Core i9 のパワーを手にしてふと思い立ち、10年ほど前に話題になったときに少し動かしてみたきりになっていた Squeak Smalltalkの多コア対応VMである「RoarVM」でまた少し遊んでみることに。

前に遊んだときは前述プロジェクトサイトで提供されている macOS 向けのバイナリーを使ったのですが、その後 10.15 Catalina から古い 32-bit アプリは動かせなくなってしまったので、今回は Linux 版を Windows 10(Boot Camp)の WSL2 上でビルドしました。当然のことながら 32-bit 版のライブラリパッケージを事前にインストールしておかなければならないことに加え、RoarVM/vm/src/makefiles/Makefile.common をちょっといじってやる必要がありましたが(具体的には g++ のコマンドオプションの LDFLAGS の位置を後ろにずらす)、あとはほぼ INSTALL.rst の記述通りで、3年前リリースの Ubuntu 18.04 でも思ったよりあっさりビルドして動かすことが可能なようです。

github.com

f:id:sumim:20210111020427p:plain
RoarVMで動作するMVC版Squeak3.7でたらい回しベンチ(この図ではTarai x: 12 y: 6 z: 0)を実行している様子

たらい回し関数による並列化効率の評価は ささださんのこちらの Ractor 紹介記事中のコードを参考にさせていただきました。

techlife.cookpad.com

Smalltalk 版のたらい回し関数は、最初は次のコードのようにクロージャーでさくっと書いて済ませるつもりだったのですが…

| tarai |

tarai := nil.
tarai := [:x :y :z |
    x <= y ifTrue: [y] ifFalse: [tarai
                value: (tarai value: x-1 value: y value: z)
                value: (tarai value: y-1 value: z value: x)
                value: (tarai value: z-1 value: x value: y)]].

[tarai value: 14 value: 7 value: 0] timeToRun / 1.0e3

なんと RoarVM 評価用の Squeak3.7 の仮想イメージ(renaissance.image)は GUI フレームワークに古い MVC だけしか用意されていない(Morphic が取り除かれて使えない)という細工がされているだけでなく、ブロックにクロージャーの振る舞いをさせるのに必要な BlockClosureクラスも取り除かれていて古典的な Smalltalk-80 や古い Squeak 同様にブロックの再帰呼び出しができない(!?)という謎仕様で運用されていることが判明したので、やむなく次のように Tarai クラスのクラスメソッド(x:y:z:)として書き直しました。

Smalltalk は ~GNU Smalltalk などの特殊な処理系を除き~ 一般にメソッド定義の構文が用意されていないので、スクリプト言語っぽい簡易コードにメソッド定義が混ざるといろいろ面倒なのです…^^;)

Object subclass: #Tarai
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Multicore-Benchmark'!

"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!

Tarai class
    instanceVariableNames: ''!

!Tarai class methodsFor: 'as yet unclassified' stamp: 'sumim 1/7/2021 01:45'!
x: x y: y z: z
    ^ x <= y ifTrue: [y] ifFalse: [self
        x: (self x: x-1 y: y z: z)
        y: (self x: y-1 y: z z: x)
        z: (self x: z-1 y: x z: y)]! !

これをシステムブラウザで定義するか、あるいは Tarai.st として保存しておき、次のベンチマークコードの最初のコメントアウトされた式でシステム内に読み込んで(file in して)から改めて次のベンチマークコード全体を選択して実行(do it)すると結果が得られます。

| queue |

"(FileStream fileNamed: 'Tarai.st') fileIn.
Transcript open"

Transcript clear.

(1 to: 16) collect: [:N |
    Transcript cr; show: N -> {
        "sequential version"
        #seq -> ([N timesRepeat: [Tarai x: 14 y: 7 z: 0]] timeToRun / 1.0e3
).

        "parallel/thread version"
        #par -> ([
            queue := SharedQueue new.
            N timesRepeat: [
                [queue nextPut: (Tarai x: 14 y: 7 z: 0)] fork
            ].
            N timesRepeat: [queue next]
        ] timeToRun / 1.0e3)
    }
]

1回実行時(あるいは HWスレッド数 N = 1)の Tarai x: 14 y: 7 z: 0 の実行時間が 155秒とかなり遅いですが、頑張って N = 16 まで回した結果が次になります。

1 -> #(#seq->152.17 #par->154.68)
2 -> #(#seq->307.371 #par->154.517)
3 -> #(#seq->458.852 #par->163.255)
4 -> #(#seq->610.838 #par->159.789)
5 -> #(#seq->765.181 #par->164.316)
6 -> #(#seq->930.125 #par->165.27)
7 -> #(#seq->1068.89 #par->169.063)
8 -> #(#seq->1227.686 #par->167.873)
9 -> #(#seq->1394.253 #par->172.45)
10 -> #(#seq->1534.125 #par->170.147)
11 -> #(#seq->1692.829 #par->170.365)
12 -> #(#seq->1883.802 #par->171.348)
13 -> #(#seq->2148.376 #par->199.649)
14 -> #(#seq->2359.443 #par->201.405)
15 -> #(#seq->2526.411 #par->204.275)
16 -> #(#seq->2684.875 #par->201.445)

しっかり並列化されていて素晴らしいですね!(小並感)

参考まで、最新の Squeak5.3 や Pharo8 では、並列化こそされませんが JIT などにより高速化された結果、N = 1 で 2.5秒程度(!!)、N = 16 の直列処理時でも 40秒程度とまずまずの性能をたたき出します。

f:id:sumim:20210111033350p:plain

f:id:sumim:20210111034429p:plain

せっかく(?)なので、Ruby3 の Ractor と、当該記事に登場する JRuby のスレッド版でも次の Ruby に焼き直したコードを使って同様の評価をしてみました。Ruby は Ractor も JRuby の Thread も N = 4 あたりから理論値を外れだして、N = 10 あたりで頭打ちになってしまうようなので今後に期待ですね。

def tarai(x, y, z)
  return y if x <= y
  tarai(
    tarai(x-1, y, z),
    tarai(y-1, z, x),
    tarai(z-1, x, y)
  )
end

require 'benchmark'
Benchmark.bm do |x|
  (1..16).each do |n|
    # sequential version
    x.report("seq-#{n}"){ n.times{ tarai(14, 7, 0) } }

    # thread version
    x.report("thr-#{n}"){
      n.times.map do
        Thread.new { tarai(14, 7, 0) }
      end.each(&:join)
    }

    if defined?(Ractor) then
      # parallel version
      x.report("par-#{n}"){
        n.times.map do
          Ractor.new { tarai(14, 7, 0) }
        end.each(&:take)
      }
    end
  end
end

f:id:sumim:20210111030219p:plain

f:id:sumim:20210111030246p:plain

ともあれ、Squeak/Pharo Smalltalk のマルチコア対応が待たれます。

(その2につづく)