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

副詞 valueLY: が並列処理にならない件を少し手を入れて解消する

前回 メッセージに織り込まれた副詞や自動詞の処理のされ方の謎が解けて、だいぶ目が慣れてきました。

実は当初、Ractorとの比較で たらい回しベンチを書いたとき、次のように副詞の valueLY: を使った実装を試してみたのですが…

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

Transcript clear.

(1 to: 16) collect: [:N |
    Transcript cr; show: N -> {
        "parallel version?"
        #'par?' -> ([(Array new: N withAll: Tarai) asEnsembleOfElements
            valueLY: [:tarai | tarai x: 12 y: 6 z: 0]] timeToRun / 1.0e3
        )
    }
]

しかしこれがまったく並列にならなかったのです。

1->#(#'par?'->4.531)
2->#(#'par?'->9.064)
3->#(#'par?'->13.063)
4->#(#'par?'->17.362)"
...

遊びはじめの頃でよくわからずそのままこの実装はボツにしたのですが、今回改めてどうしてか気になったので調べてみることにしました。ずばり、valueLY: の実際の処理を担当する Sly3ModValuely>>#extendOperandTuples:operand:membersOrNil: の定義をよく見てみると…

Sly3ModValuely >> extendOperandTuples: operandTuplesSoFar operand: operand membersOrNil: operandMembers
    | r mems |
    mems _ operandMembers ifNil: [OrderedCollection with: operand] ifNotNil: [operandMembers].

    ^ operandTuplesSoFar
        ifEmpty: [mems collect: [:m| OrderedCollection with: (parameter value: m)]]
        ifNotEmpty: [ 
            operandTuplesSoFar size = mems size ifFalse: [self error: 'mismatch in number'].
            r _ OrderedCollection new.
            operandTuplesSoFar with: mems do: [:ot :m| r addLast: (ot copy addLast: (parameter value: m); yourself)].
            r]

なんということもなく、普通に #collect: や #with:do: を使っていて、並列に処理しようとする気概が感じられません。^^; これでは直列処理になるのは当然です。

せっかくなので並列処理になるように改変してみましょう。 前者は #parallelCollect: という並列化バージョンがすぐ見つかりましたのでこれと置き換えればよさそうですが、後者については #with:parallelDo: はもちろん #parallelDo: が探しても見つかりません。

いろいろ当たってみたところ #doInParallel: というのが見つかりました。ところがこれの定義を見てまたびっくり。

Collection >> doInParallel: aBlock
    self parallelCollect: aBlock

手抜きにも程があるだろうと少々呆れつつ、#parallelCollect: を参考に次のように書き直してみました。

Collection >> doInParallel: aBlock
    | task  barrier |
    self ifEmpty: [^ self].
    (Sly3 serializeForDebugging or: [self size = 1]) ifTrue: [^ self do: aBlock].
    barrier _ RVMBarrier new signalsNeededToPass: self size.
    self do:[:each|
        task _ [
            [aBlock copy fixTemps value: each] ifCurtailed: [barrier signal].
            barrier signal] asSlyMemberProcess.
            task resume].
    barrier wait

あらためて、Sly3ModValuely>>#extendOperandTuples:operand:membersOrNil: を次のように書き換えます。例によってなぜかクロージャではなく(つまり、古典的 Smalltalk-80 の BlockContext に先祖返り)させれたこのイメージのブロックは再入ができないので、#parallelCollect: および #doInParallel: の引数のブロックないで parameter をコール(#value:)しているところは忘れずに事前に copy fixTemps しておきます。

Sly3ModValuely >> extendOperandTuples: operandTuplesSoFar operand: operand membersOrNil: operandMembers
    | r mems |
    mems _ operandMembers ifNil: [OrderedCollection with: operand] ifNotNil: [operandMembers].

    ^ operandTuplesSoFar
        ifEmpty: [mems parallelCollect: [:m| OrderedCollection with: (parameter copy fixTemps value: m)]]
        ifNotEmpty: [ 
            operandTuplesSoFar size = mems size ifFalse: [self error: 'mismatch in number'].
            r _ OrderedCollection new.
            (1 to: mems size) doInParallel: [:idx | r addLast: ((operandTuplesSoFar at: idx) copy addLast: (parameter copy fixTemps value: (mems at: idx)); yourself)].
            r]

さて、これでどうでしょう。

1->#(#par->4.57)
2->#(#par->4.476)
3->#(#par->4.539)
4->#(#par->4.719)"
...

もくろみ通り、並列化成功です。

なぜ valueLY: が並列化されていなかったのかは謎ですが、#doInParallel: の実装を見るにつけいろいろと察せられ、なんだかちょっと残念な感じになってきました。

なお、serialLYvalueLY: と書いても副詞 valueLY: を同じ副詞の serialLY の修飾とは見なされないようで、直列化はされませんでした。

あと冒頭の valueLY: はレシーバーのアンサンブルの修飾になるわけですが、各引数のアンサンブルにも個別に valueLY: 修飾が可能なようです。

たとえばいささか無理矢理な例ですが…

[%{Tarai. Tarai} valueLY: [:tarai | tarai x: 12 y: 6 z: 0. tarai]
    x: %{12. 12} valueLY: [:x | Tarai x: x y: 6 z: 0. x]
    y: %{6. 6} valueLY: [:y | Tarai x: 12 y: y z: 0. y]
    z: %{0. 0} valueLY: [:z | Tarai x: 12 y: 6 z: z. z]] timeToRun

というように書けば、レシーバーの %{Tarai. Tarai} というアンサンブルに対し、最初の valueLY: が適用され、その返値(のアンサンブルの各要素である Tarai )に対し送られる x: ~ y: ~ z: ~ というメッセージの各引数についても、それぞれの直後に書いた valueLY: で修飾することができる(その結果、セレクタとしては valueLY:x:valueLY:y:valueLY:z:valueLY: になる!)ので、つごう4+1=5回の 2つの並列のたらい回しを回す式の記述が可能になります。

計測してみると、前述の Sly3ModValuely>>#extendOperandTuples:operand:membersOrNil: 等への細工を済ませておけば、レシーバーに加え、各引数に対する valueLY: 修飾もそれぞれ2つの並列で行われるようです。ただレシバーと各引数に対する4つの修飾に伴う処理は Sly3 の並列化の管轄外なのか直列で処理されます。

"すべての valueLY: 処理をコメントアウト #ori=> 4624 #mod=> 4609 "
"レシーバーを修飾する valueLY: のみ実行 #ori=> 13178 #mod=> 9347 "
"レシーバーと第一引数を修飾する valueLY: のみ実行 #ori=> 22171 #mod=> 13984 "
"レシーバーと第一、第二引数を修飾する valueLY: を実行 #ori=> 31017 #mod=> 18245 "
"すべての valueLY: 処理を実行 #ori=> 39734 #mod=> 22848 "
"参考:たらい回しベンチ1回のみ実行時の計測"
[Tarai x: 12 y: 6 z: 0] timeToRun "=> 4680 "

今回はここまで。valueLY: による引数の修飾の記述は、Smalltalk と違い、キーワードメッセージでも(二つ目以降のキーワードの頭文字を大文字にするルールを設けることで、括弧を使わずに―)複数同時に使用可能な SELF を彷彿とさせますね。David Ungar 率いるプロジェクトだけに。