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 率いるプロジェクトだけに。

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

前回、RoarVM 専用並列処理特化 DSL である Sly3 について、「アンサンブルが同名メソッドを持っていなければ、副詞 IndividualLY が無くてもメッセージはアンサンブル内の各要素に委譲される」と書きましたが、あれは嘘でした。すみません。

blocks copy fixTemps

と複数のブロックを収めたアンサンブルである blocks に対して、アンサンブルにない BlockContext 固有の fixTemps はともかく、誰でも反応できるはずの copy を送っても IndividualLY になるのはおかしいな…と気になって調べてみたところ、完全な思い違いが判明しました。お恥ずかしい。

そもそもこの勘違いのきっかけは、Sly3Ensemble に #doesNotUnderstand: が(再)定義されているのを見つけて、てっきり自分が理解できないメッセージについてのみその要素にメッセージを転送する、よくある仕組みだろうと早合点したことでした。

実際は、アンサンブルを構成する Sly3Ensemble>>#members: 内で Object>>#primitiveSetExtraWordSelector: および Object>>#primitiveSetExtraPreheaderWord: をコールすることでプリミティブレベルで自身の情報を書き換えることで、自身に送られたメッセージは(先述の、よくある doesNotUnderstand: の再定義による仕組みを介することなしに) members に収められたコレクションに sentToEnsemble: aMessage をコールするかたちで無条件に委譲される仕組みになっていました。(これだけだとアンサンブルを構成するコレクションに委譲されるだけですが、さらに先の処理で一定の条件を満たした場合に各要素に委譲されるようです。)

Sly3Ensemble >> members: aCollection
    "crude check:"
    self primitiveSetExtraWordSelector: #sentToEnsemble:.
    aCollection primitiveSetExtraPreheaderWord: self.
    members _ aCollection.
    ^ self

ではなぜ通常は使う必要がない #doesNotUnderstand: が再定義されているかというと、これも当該メソッドのソースにちゃんと説明が書いてありました。^^;

Sly3Ensemble >> doesNotUnderstand: aMessage
    "Try to process this message as an ensemble dispatch message to help in the case of inlined primitive calls  like perform:with:with:"
    ^ Sly3EnsembleMessageDispatcher dispatch: (Sly3UnprocessedEnsembleMessage fromMessage: aMessage theEnsemble: self)

通常のメッセージ式ではなく、#perform: などを使ってアンサンブルにメッセージが送られた場合であっても、同様の仕組みが(もとより、Sly3Ensemble に定義されていないメソッドのコールに限られるため、不完全ではありますが―)機能するように備えたもの…ということらしいです。

ただ残念ながら、デバッガーで使われるインタープリターシミュレーターにはこうした配慮が行き届いていない(あるいは当該機構によりインタープリターシミュレーターがバグる?)ようで、結果的にデバッガーでステップ実行したときと通常実行したときで動きが異なる事態が発生してしまうみたいなので要注意です。

関連してデバッガーについては、これを動作させようとすると並列処理に起因すると思われるノーティファイアーの嵐が出まくって操作不能に陥ることが多々あり、やはりプロセス(スレッド)を多コア対応にできても Squeak Smalltalk 環境を既存のコードのまま運用するのは難しく、全体の書き直しが必要でそれはきっと大変な作業になるのだろうな…と実感させられます。RoarVM/Sly3 評価用の renaissance.image が かなり手間をかけてMVC ベースや、ブロックがクロージャーでなくすなど過去の Smalltalk の状態に戻してしてあったりするのも、並列処理に起因するトラブルにできるだけシンプルな対応で済ませたいがためだったのかもしれませんね。

次回へ続く。

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

前回 の続き。

多コア用 RoarVM 向けには Sly3 という並列処理を記述するための DSL も同時に開発されていました。もともと Ly という Smalltalk上に実装された JavaScript ライクな文法の並列処理言語とその処理系が作られ、それを Smalltalk 内で DSL化したのが Sly で、Sly3 はその3サイクル目ということらしいです。

この Ly/Sly や RoarVM はプロトタイプベース・オブジェクト指向の祖として知られる SELF言語を開発者した David Ungar とテクトロニクス社出身の Sam S. Adams らが参加したIBM基礎研究所・ポートランド州立大学、ブリュッセル自由大学の共同研究「ルネッサンス」プロジェクトの成果物です。

www.stefan-marr.de

Ly/Sly は「アンサンブル」と呼ばれる特殊なコレクションにオブジェクトを収めておき、それにメッセージを送ることで要素であるオブジェクトに並列処理をさせる仕組みになっています。このとき送るメッセージにすこし細工があって、最終的にコールされるメソッドをどのように実行するのかをメソッド名を修飾する副詞や動名詞としてメッセージに織り込むことで並列処理を制御します。

たとえば、%{1. 2. 3} というアンサンブルに含まれる要素に対して %{10. 20. 30} というアンサンブルの要素を加える(plus:)場合、「総当たりで」(roundly)という副詞で修飾したセレクタ(通常の Smalltalk ではメソッド名と同義ですが、Sly3 ではそういうメソッドがあるわけではないのであくまでメッセージの一部)を合成して用いた次のような式として表現できます。

%{1. 2. 3} plusRoundLY: %{10. 20. 30}

"=> %{11. 12. 13. 21. 22. 23. 31. 32. 33. } "

このときセレクタを修飾する副詞は語尾の ly を大文字で LY と記述するルールになっていて、また、この副詞の語尾の ly がこの言語の名前の由来なのだと思います。

なお、ここで修飾される側の「plus:」は、通常であれば「+」を用いたいところですが、残念ながら Smalltalk の文法上の制約からセレクタを記号とをアルファベットの混成にする(roundLY+、+roundLY: など)ことができないので Object >> #plus: といったキーワードセレクタのメソッドとして別に定義しておく必要があります。念のため。

ちなみに { } は、各要素を生成する式(もちろんリテラル式も可)をピリオドで区切って連ね、それらを { } でくくることで動的に配列を定義できるよう Squeak Smalltalk に新たに(と言っても 1990年代なのでずいぶん前ですが─)追加された記法です。それまでの Smalltalk-80 では、リテラルを要素とする配列定義( #('this' is $a 10) など)はありましたが、動的に要素を定義したければ配列等のクラスメソッドとして用意された with: 、with:with: 、with:with:with: … を使う必要がありました。{ } による動的配列定義記法はその後、他の Smalltalk 処理系にも広まりました。

#('this' is $a 10) collect: [:each | each hash]

"=> #(219033213 218806809 97 10) "
(Array with: 'siht' reversed with: 'is' asSymbol with: 'abc' first with: 3 + 7)
    collect: [:each | each hash]

"=> #(219033213 218806809 97 10) "
{'siht' reversed. 'is' asSymbol. 'abc' first. 3 + 7} 
    collect: [:each | each hash] 

"=> #(219033213 218806809 97 10) "

Sly3 ではこれの頭に % を付すことでアンサンブルを生成する式として利用できます。他にも、Sly3Ensemble class >> #withMembersFrom: や Collection >> #asEnsembleOfElements が使えます。

%{'this'. #is. $a. 10} hashIndividualLY

"=> %{219033213. 218806809. 97. 10. } "
(Sly3Ensemble withMembersFrom: #('this' is $a 10)) hashIndividualLY

"=> %{219033213. 218806809. 97. 10. } "
#('this' is $a 10) asEnsembleOfElements hashIndividualLY

"=> %{219033213. 218806809. 97. 10. } "

アンサンブル自身ではなく、それに含まれる要素それぞれに hash を送るために副詞 individually で修飾するため、hashIndividualLY と記述しています。

このように副詞が LY なのに対し、動名詞は ING で、アンサンブルの要素の畳み込み処理を指示するときに使います。

%{1. 2. 3. 4} totallING "=> 10 "

使用できる副詞と動名詞は Sly3Mod~ で始まるクラス名から調べられます。

Smalltalk allClasses select: [:class | class name beginsWith: 'Sly3Mod']

"=> an OrderedCollection(
    Sly3ModAnding
    Sly3ModAveraging
    Sly3ModCollectionly
    Sly3ModConcatenating
    Sly3ModDuplicatively
    Sly3ModEnsembling
    Sly3ModGathering
    Sly3ModIndividually
    Sly3ModMaxing
    Sly3ModMining
    Sly3ModOring
    Sly3ModPlainly
    Sly3ModRandomly
    Sly3ModRoundly
    Sly3ModSelecting
    Sly3ModSelectively
    Sly3ModSerially
    Sly3ModStandardDeviationing
    Sly3ModTotalling
    Sly3ModValuely
    Sly3ModWholly) "

Sly3ModStandardDeviationing のように実質使えないものもあるので要注意ですが…^^;

さて、ざっと Sly3 の様子がわかったところで、前回の たらい回しベンチを Sly3 で書き直してみたのがこちらです。

| blocks |

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

Transcript clear.

(1 to: 16) collect: [:N |

    blocks _ Sly3Ensemble withMembersFrom: (Array new: N withAll: [Tarai x: 12 y: 6 z: 0]).
    blocks _ blocks copy fixTemps.

    Transcript cr; show: N -> {
        "sequential version"
        #seq -> ([blocks serialLYvalue] timeToRun / 1.0e3).
        "parallel version"
        #par -> ([blocks value] timeToRun / 1.0e3)
    }
]

なぜかクロージャーでなくなってしまったブロックを繰り返し評価するために copy して fixTemps する必要があるなど余計な処理が入っていますが、実にすっきりとかけていて素晴らしいですね。(ちなみに、アンサンブルが同名メソッドを持っていなければ、副詞 IndividualLY が無くてもメッセージはアンサンブル内の各要素に委譲されるようです。ん?…でも copy は?→あとで調べておきます^^; → 勘違いしていました

結果はこちら。Tarai x: 14 y: 7 z: 0 だとキツいので^^; 上のコードのとおり Tarai x: 12 y: 6 z: 0 を使った結果になります。あしからず。

1->#(#seq->3.574 #par->3.371)
2->#(#seq->6.518 #par->3.299)
3->#(#seq->10.264 #par->3.792)
4->#(#seq->13.359 #par->3.429)
5->#(#seq->16.954 #par->3.662)
6->#(#seq->19.603 #par->3.519)
7->#(#seq->23.365 #par->3.467)
8->#(#seq->26.263 #par->3.753)
9->#(#seq->30.277 #par->3.847)
10->#(#seq->33.913 #par->3.645)
11->#(#seq->37.356 #par->3.751)
12->#(#seq->40.46 #par->3.933)
13->#(#seq->43.841 #par->3.717)
14->#(#seq->46.286 #par->3.729)
15->#(#seq->50.822 #par->3.932)
16->#(#seq->54.656 #par->3.793)

[追記] よく考えてみたところ、アンサンブルに送る(ことで、その要素に送られる)メッセージは単項メッセージでなければいけないということはなかったので、アンサンブルに Tarai を必要な数だけ入れておいて x: 12 y: 6 z: 0 を送るのでもよいのではないか…と気づいたので書き直してみました。逐次実行は同じように副詞 serialLY で修飾しています。

| tarais |

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

Transcript clear.

(1 to: 16) collect: [:N |

    tarais _ (Array new: N withAll: Tarai) asEnsembleOfElements.

    Transcript cr; show: N -> {
        "sequential version"
        #seq -> ([tarais serialLYx: 12 y: 6 z: 0] timeToRun / 1.0e3).
        "parallel version"
        #par -> ([tarais x: 12 y: 6 z: 0] timeToRun / 1.0e3)
    }
]

こちらの方がずっと Sly3 らしくていいですね!

結果もほぼ同じです。

1->#(#seq->3.055 #par->3.332)
2->#(#seq->6.437 #par->3.164)
3->#(#seq->9.861 #par->3.605)
4->#(#seq->13.64 #par->3.496)
5->#(#seq->15.791 #par->3.378)
6->#(#seq->19.625 #par->3.938)
7->#(#seq->22.378 #par->3.691)
8->#(#seq->25.633 #par->3.525)
9->#(#seq->29.435 #par->3.655)
10->#(#seq->32.004 #par->3.664)
11->#(#seq->34.929 #par->3.874)
12->#(#seq->37.529 #par->3.629)
13->#(#seq->41.2 #par->3.761)
14->#(#seq->47.115 #par->4.201)
15->#(#seq->49.703 #par->3.783)
16->#(#seq->53.233 #par->3.824)

次回へ続く

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につづく)

Smalltalk-72で学ぶOOPの原点:まとめ

サンプルコード「じゃんけんゲーム」(@nrslibさんのSIMULA版を移植)の続き)

アドカレのかたちを借りて、アラン・ケイの“オブジェクト指向”というアイデアをもとに(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72でいろいろ遊んでみたわけですが、お楽しみ頂けましたでしょうか。

絵文字などが多用されているコードの見た目の時点でもうすでにかなりユニークなSmalltalk-72ですが、純粋に言語として見ても、今でも十分異端とされる我々の知るSmalltalk-80以降の現在に至る実装(例えばPharo)のそのまたさらに遥か斜め上を行く“メッセージング”の徹底具合とその実現方法に度肝を抜かれたかと思います。

今のSmalltalkを含めて、Smalltalk-76以降のSmalltalkがパフォーマンスを優先してそれを選択したが故に、現在主流のメッセージングのOOPLの実装は「メンバー関数を“メソッド”と呼び、その動的コール&風変わりな例外処理(メソッドがないときに発動)の組み合わせを“メッセージ”と称する」のが唯一の正解とされてしまいがちですが、実装のバリエーションはいろいろあってもっと試されるべきなんだよ!(「だったんだよ!」ではなく現在進行形なのも重要!)ということを少しでもお伝えできたならさいわいです。


以下、このシリーズを通じてSmalltalk-72で遊んで気がついたことをつらつらと。(順不同。逐次追加)

  • トークン列のパースをメッセージングに見立てたアイデアは斬新だが、今のSmalltalkのようにコンパイル言語にはしにくい(実行速度は稼ぎにくい)かも。
  • 文法を定義できるのは強力だが、コンテキストによってコードの意味が変わるのは読むときつらい(つーか、字面を追うだけでは無理)。
  • 動的にコードを読む(実行時の動きを追う)にしても、今のSmalltalkのような強力なデバッガーが無いのがつらい(モードレスエディタ同様に発明前なので当然だが──)。そもそも、現在のOOPLに至ってもなおこれ無しに動的言語のコーディングや既存コードの解析をしている人を尊敬する!
  • このSmalltalk-72に影響を受けたカール・ヒューイットのアクターの実装(PLASMA)がどうなっていたか調べてみたい。Erlang/Elixirのメッセージングの実装がそれにどの程度影響を受けたものか確かめたい。
  • いろいろ工夫はできるが継承がないのはやっぱりつらい。継承にするかはともかく、コード(あるいはコード片)をオブジェクト間で共有する何らかの機構は(ユーザーサイドのノウハウの蓄積に頼るのではなく──)処理系サイドで用意していただきたい!
  • このあと発明されたコピペを含むモードレスエディタは偉大!(LivelyWebをもうちょっと勉強すればペースト機能くらいは追加できるかも…)
  • 知らないメッセージが来ると、冒頭のトークンをアクティベートして新しいコンテキストで処理が始まるのは斬新。
  • ただ、今のSmalltalkのようにオブジェクトにメッセージを送る(正味は動的な関数コールなのはさておき──)とそれがトリガーになる感じのほうが好き。
  • なんかこう(今のSmalltalkにおいてオブジェクトがいつもそこにあって自律してるイメージではなく──)いちいち処理系がしゃしゃり出てくる感じ(?)がSmalltalk-72方式にはある。
  • あと、知らないメッセージを無視してしまうことになるので、フォールスルー処理でできることが限定される。これはよくない。→あ、これ、ピーク(鍵穴マーク)を使えばできるのか!
  • でも、ネットワークや細胞間での通信とのアナロジーからはスルーする方がよく合致する。ここらへんアラン・・ケイ的にどう考えているのか知りたい。
  • 記号は1文字でトークンになるところがミソであるが(Smalltalk-76も同じ。なお、-80以降の今のSmalltalkでは記号が続くとまとめてトークンになるので、<=>とか新しいメソッドを追加できる──)、「=<」「<=」の代わりに「≦」を用いる等で必然的に多くの記号が必要になり、ともするとコードの読み下しのしにくさ、またLivelyWeb版に関してはWebブラウザにキーアサインを奪われることでエミュレーターの制約につながってしまっている。
  • 代入(☞<変数名>←<値>)や(Smalltalk-80以降は別の方法で戻ったが──)条件分岐(<条件式>⇒(<非偽時処理>)<偽時処理>)も一貫して意味的にはちゃんとメッセージ式になっているのはさすが。
  • アクションやクラスが実質クロージャーなのがびっくり。
  • メソッドが独立した関数ではなく、巨大なIF式の中にネストされたひとつのパターンマッチのコード片に過ぎないのもびっくり。
  • アクションやクラスがアクティベートされる(トークン列からオブジェクトだと判断される)とすぐにコードが実行されてしまうのはわかりにくいし、第遺丘オブジェクトとして扱いにくい。
  • 処理系の細かな挙動を理解するにあたっては、Smalltalk-72のオリジナルの実装者であるダン・インガルス自身によるSqueak版Smalltalk-72の実装が(若干齟齬はあるものの──)非常に参考になる。ここでもやはり今のSmalltalkのデバッガーは必須のツールであった。

Smalltalk-72で学ぶOOPの原点:サンプルコード「じゃんけんゲーム」(@nrslibさんのSIMULA版を移植)

アラン・ケイの“オブジェクト指向”というアイデアをもとに(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72で遊んでみるシリーズです(なお最新のSmalltalkについては Pharo などでお楽しみください!)。他の記事はこちらから→Smalltalk-72で学ぶOOPの原点 Advent Calendar 2019 - Qiita


サンプルコード「ローンシミュレーション」(report、demand、payment)の続き)

じゃんけんゲーム

以前、@nrslibさんとSmalltalk-72およびSIMULAの話をした際に、

その流れで即座に作られたSIMULA版(Cimを使用)のじゃんけんゲーム

こちらを比較のためSmalltalk-72に移植させていただきました。

インスタンス変数dispが同名のグローバル変数と被るのでこれをdisplayに変更した以外は、なるべく忠実に再現したつもりですが、ちょっとだけ余計なひねり(後述)を入れています。どうぞあしからず。なお、クラス名を大文字スタートにしていなかったり、変数名等を含めキャメルケースにしなかったのは、Smalltalk-72の他のコードの雰囲気に合わせただけで、言語の制約によるものではありません。いずれも普通に使えますので、お手元で試される際は読みやすいように適宜書き換えてください。

まず、jankenクラスとenglishdisplayおよびjapanesedisplayアクションです。これらの名前はそのまま~dispのままでよかったかと思いますが、先述のインスタンス変数等のdispdisplayに変更するのにあわせてうっかり変えてしまいました。

f:id:sumim:20191227205335p:plain
「janken」「englishdisplay」「japanesedisplay」の定義

Smalltalk-72にはそもそも継承機構が無いため、englishdisplayjapanesedisplayに共通の抽象クラスであるdisplayは定義していません。もしテンプレートメソッドパターンが使われていたら、このシリーズでも取り上げたクラス変数を使ったメソッド(正確にはアクション)の共有テクニックなどを活用しようかとは思っていたのですが、今回はその出番はありませんでした。

そこで“ひねり”を思いついて、この2つはクラス(実際に使うのはそのインスタンス)ではなく、アクション(インスタンス生成能を持たないクロージャーのようなもの)で実装してみました。クラスもそうですが、アクションは名前を書くだけで実行されてしまうので、その本体を変数に代入したり引数として渡す際には参照(#~)で渡さないといけなくなるなど、かえって面倒になったような気もしますがまあよしとしましょう。^^;

呼び出し用のjankengameアクションの定義と実行例はこちらです。

f:id:sumim:20191227210152p:plain
「jankengame」アクションの定義と実行例

実行時に「1」というメッセージを送るとenglishdisplayが、それ以外ではjapanesedisplayが選ばれる挙動は(プログラムの起動方法、オプションをメッセージとして与える等の手段は別にして──)元のSIMULA版と同じです。

シリーズ通して読んでいただいたなら、特に説明は不要かと思いますが、いちいち読んでいられない! and/or 分かりにくいところ等あれば補足しますのでコメント欄やTwitterのメンション、DM等で気軽にリクエストください。

まとめへ続く)

Smalltalk-72で学ぶOOPの原点:サンプルコード「ローンシミュレーション」(report、demand、payment)

アラン・ケイの“オブジェクト指向”というアイデアをもとに(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72で遊んでみるシリーズです(なお最新のSmalltalkについては Pharo などでお楽しみください!)。他の記事はこちらから→Smalltalk-72で学ぶOOPの原点 Advent Calendar 2019 - Qiita


サンプルコード「joe the box」(is、'sの追加)の続き)

ローンシミュレーション

「Open the ST-72 Manual」でダウンロードして表示することができるSmalltalk-72操作マニュアルのPage 84からにあるローン返済シミュレーションを書いてみます。

元コードではfloatクラスに$メソッドを追加して小数点以下を丸める操作ができるようにしているのですが、あいにくLivelyWeb版のSmalltalk-72(と、それに手を加えたちょい直し版)では「$」のグリフを持つ記号を入力できないので、代わりにdollarメソッドとして追加することにします。既存クラスへのメソッド(パターンマッチのコード片)の追加にはaddtoアクションを使います。

f:id:sumim:20191227195000p:plain
「float」クラスへの「dollar」メソッドの追加と動作確認

show floatでコード片が最後に追加されているのを実際に見ることができます。

f:id:sumim:20191227195152p:plain
「dollar」メソッド(パターンマッチのコード片)が追加された「float」クラス

打ち間違いなどあってうまく動かないときはedit floatで組み込みのエディタを起動しそこで修正してください。なお、組み込みエディタは前述の“ちょい直し版”でのみ操作可能です。ご注意あれかし。

次にreportdemandというアクションを定義します。これらはそれぞれキャプション付きの出力と入力機能を提供します。

f:id:sumim:20191227200531p:plain
キャプション付きの出力機能を提供する「report」アクション

demandは、as <文字列>とメッセージに続けると指定した文字列をキャプションにし、これを省略すると代入する変数名がキャプションとして表示され、直後にアルトを模したアイコンがプロンプトとして表示されるので値を入力してdo-it(「\」キー)すると値が入力されます

f:id:sumim:20191227200617p:plain
キャプション付きの入力機能を提供する「demand」アクション

最後に本体のpaymentアクションを定義すれば完了です。あいかわらず「$」マークが入力できないので、キャプションで「$」を表示するために、いったん一時変数dollarに長さ1文字の文字列を代入し(☞dollar ← string 1.)、それを「$」マークの割り振られたコード27に置き換えておき(dollar[1] ← 20)、適宜キャプションの最後に追加(+ dollar)というようなことをしています。

定義後にpamentと入力してdo-it(「\」キー)すると、借入額、年利、返済年数、年当たりの返済回数を尋ねられるので入力すると結果が出力されます。

f:id:sumim:20191227202159p:plain
「payment」アクションの定義と実行例

サンプルコード「じゃんけんゲーム」(@nrslibさんのSIMULA版を移植)に続く)