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)

次回へ続く