Smalltalk-72で遊ぶOOPの原点:宇宙船を描く(たぶんこうなんじゃないか編)
アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。
今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームを Smalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita
エスパー力を発揮して宇宙船完成図を予想する
Smalltalk-71 のコードをもう一度確認してみましょう。一辺の長さが 2 もしくは 4 で宇宙船胴体とおぼしき扁平6角形が描かれたあと(青字)、2√3 → 2 → 2 → 2√3 の長さで4本の線分が引かれています(オレンジと赤字)。
to ship :size penup, left 180, forward 2 * :size, right 90 forward 1 * :size, right 90 pendown, forward 4 * :size, right 30, forward 2 * :size right 120, forward 2 * :size right 30, forward 4 * :size right 30, forward 2 * :size right 120, forward 2 * :size left 150, forward :size * 2 * sqrt 3 left 330, forward :size * 2 right 60, forward :size * 2 left 330, forward :size * 2 * sqrt 3 penup, left 90, forward :size, right 90, forward 2 * :size end to
[2023-12-04 追記:最後の right 60
との誤りと思われていた記述は、誌面では正しく right 90
で OCR の誤認識でしたので、以降もしれっと無かったことにしています。あしからず^^;]
ここで、あらぬ方向に伸びている最後の4本の線分について、その角度はともかく長さについては正しいと仮定します。
さらに、オレンジの線については角度も正しいとすると、最後の 2√3 の長さの赤線は、オレンジの線と同様に宇宙船の右側に沿って描かれる必要がある、と考えるのは妥当でしょう。
残りの二本の赤線の長さは 2 、オレンジと最後の赤の線分の長さが 2√3 であることをあわせると、宇宙船胴体底部の頂点に接するように 60度の角を作って2本の長さ 2 の線分が描かれるとすれば、収まりが良さそうです。
ところで、330
という数は 360 - 30
から求められた数値と考えることもできます。タートルグラフィックスで例えば正三角形を描く場合、60
度の角を作るために 180 - 60
度、つまり 120
度 曲げなければならないのですが、この 180
を 360
とうっかり取り違えてしまったとしたら…と想像するのはうがち過ぎでしょうか?
つまり、330
は 180-30
の 180
を 360
に取り違えで、続く 60
は 180-60
の 180
から引くのを忘れたよくあるミス…と解釈すると、それっぽく説明がつきそうではあります。
もとより、扁平六角形はきちんと描けているので、どうしてここに来て2度までも違う方法でミスったかという疑問は残りますが…^^;
[2023-12-04 追記:ぼんやりと図を眺めていたら、左に 330度、右に 60度、左に 330度…とよくある「180
から引くのを忘れる」勘違いをしたと解釈する方が自然では?思えてきました。その場合は一貫していて「2度までも違う方法」ではないですね^^; もっとも依然「どうしてここに来てミスったか?」という疑問は残りますが…]
ともあれ、Smalltalk-71 のコードは、とりあえずは次のように読み替えることにいたしましょう。
to ship :size penup, left 180, forward 2 * :size, right 90 forward 1 * :size, right 90 pendown, forward 4 * :size, right 30, forward 2 * :size right 120, forward 2 * :size right 30, forward 4 * :size right 30, forward 2 * :size right 120, forward 2 * :size left 150, forward :size * 2 * sqrt 3 left 150, forward :size * 2 right 120, forward :size * 2 left 150, forward :size * 2 * sqrt 3 penup, left 90, forward :size, right 90, forward 2 * :size end to
したがって、Smalltalk-72 へのトランスパイルは次のようにするのがよさそうです。
to sqrt x (:x. !1.73) to ship size ( @ penup turn 180 go 2 * :size turn 90 go 1 * size turn 90 pendn go 4 * size turn 30 go 2 * size turn 120 go 2 * size turn 30 go 4 * size turn 30 go 2 * size turn 120 go 2 * size turn `150 go (sqrt 3) * 2 * size turn `150 go 2 * size turn 120 go 2 * size turn `150 go (sqrt 3) * 2 * size penup turn `90 go 1 * size turn 90 go 2 * size ) disp clear @ home penup goto 200 100 pendn. ship 10
(スラスターの火炎を描く(準備編)へ続く)
Smalltalk-72で遊ぶOOPの原点:宇宙船を描く(失敗編)
アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。
今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームを Smalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita
まずは Smalltalk-71 の「ship」を Smalltalk-72 にトランスパイル!
手始めに簡単そうな冒頭の to ship …
を Smalltalk-72 に書き直すところから始めてみましょう。
Smalltalk-71 のコードの該当部分を OCR したものがこちらです。
to ship :size penup, left 180, forward 2 * :size, right 90 forward 1 * :size, right 90 pendown, forward 4 * :size, right 30, forward 2 * :size right 120, forward 2 * :size right 30, forward 4 * :size right 30, forward 2 * :size right 120, forward 2 * :size left 150, forward :size * 2 * sqrt 3 left 330, forward :size * 2 right 60, forward :size * 2 left 330, forward :size * 2 * sqrt 3 penup, left 90, forward :size, right 90, forward 2 * :size end to
[2023-12-04 追記:最後の right 60
との誤りと思われていた記述は、誌面では正しく right 90
で OCR の誤認識でしたので、以降もしれっと無かったことにしています。あしからず^^;]
to
は Logo のプロシージャ定義キーワードの to
を模したものと思われます。続く :size
は ship
をコールしたときの仮引数、それ以降の参照にも同じ :size
を使うことも含め(カンマによる式の区切りや end to
などの一部の例外を除けば)、この Smalltalk-71 のコードはほぼ Logo のそれを再現したものになっていますね!
ところで Smalltalk-72のクラスは実質、クロージャーのようなもので実現されています。インスタンスを生成しない(すなわち、isnewをメソッドに含まない)クラスを「アクション」と呼んで、よく関数代わりに使います。クラスの定義はアクション to
へのメッセージ送信で行いますが、見た目は Smalltalk-71 同様、Logo の to
構文と似たものになっているように見えます。
しかし、Smalltalk-72 のクラスには(関数のようにコールされはするものの)特に仮引数宣言のための構文のようなものは用意されていません。そこで、Smalltalk-71で書かれたこのship
を再現するには、①テンポラリ変数としてsize
を宣言(to ship size (…
)し、②引数として送られた数をsize
の初回参照時にメッセージとしてフェッチ :size
(念のため、LogoやSmalltalk-71と違い、Smalltalk-72でこの記述は「アクション :
へのシンボル size
の送信」という意味を持ちます!)、③以降は普通にsize
で参照…というふうに書くことになります。
次いで、Smalltalk-71で書かれた ship
に登場する penup
、pendown
、left
もしくは right
、forward
も、Logoのタートルグラフィックス用のコマンドと一致します。
Smalltalk-72 では、これらはタートル(turtle
のインスタンス。グリフはスマイリー☺
の @
記号に代入されている)に対する penup
、pendn
、turn
(時計回りの right
に相当。引数の符号を反転すればleft
に)、go
の送信に読み替えられます。また、Smalltalk-72 の turtle
ではメッセージのカスケード(たたみかけて送ること)ができるので、Smalltalk-71 でカンマで表されているコマンドの区切りを、Smalltalk-72で式の区切りとしていちいち置き換える必要はなさそうです。
Smalltalk-71 のコードで forward
の引数に 1 * :size
とわざわざ 1
に :size
をかけているのは、:size
が整数でなかった場合にそれを整数化するためでしょうか? しかし、後半で * 2
となっていたり、1 *
を付けずに :size
だけ参照する記述も見受けられるため、これらを書き損じとするか、それ以前に 整数 *
などとする必要がそもそも有るのか無いのかについては、まあ、謎です^^;
Smalltalk-72 のタートルへのメッセージは(Logo のタートル制御コマンドと同様に)パラメータが小数でもよいので整数化の必要はありません。ただ、sqrt 3
のような少数が含まれる式を計算するとき size * 2 * (sqrt 3)
の順のままではうまく計算できない(整数 * 小数
は 整数
になってしまう)ことを鑑みて、sqrt 3
を先頭に置いた (sqrt 3) * 2 * size
の順に統一することにします。
もっともこの順序にした場合、Smalltalk-72 の演算は右結合なので、もし size
が少数でも 2 * size
の段階で整数に丸められてしまいます。1 * size
は「size
を(他は 2倍や 2√3倍かもしれないが、ここでは)等倍で使用する」というアピール以外の意味は失われ、size
も整数を想定する必要が生じますのであとで見直さないといけない…かも^^;
ところで、sqrt
は Smalltalk-72 組み込みのアクションには無いので定義しないといけませんが、見たところ sqrt 3
しか使われてないので、当面は 1.73
を返す仮実装でやり過ごすことにします。
この方針で Smalltalk-72 に変換すると、ship
のコードはこのように変換できます。
Snippet3ウインドウなどにコピペする際は、下のテキストをご利用ください。(ここでは文字化けして読みづらいですが、Snippets3ウインドウなどに空行を設けてペーストすることで、文字化けもほぼ解消されます。)
to sqrt x (:x. !1.73) to ship size ( @ penup turn `180 go 2 * :size turn 90 go 1 * size turn 90 pendn go 4 * size turn 30 go 2 * size turn 120 go 2 * size turn 30 go 4 * size turn 30 go 2 * size turn 120 go 2 * size turn `150 go (sqrt 3) * 2 * size turn `330 go 2 * size turn 60 go 2 * size turn `330 go (sqrt 3) * 2 * size penup turn `90 go 1 * size turn 90 go 2 * size ) disp clear @ home penup goto 200 100 pendn. ship 10
ペースト後、プロンプトと同じAltoアイコンの直後でダブルクリックにならないようにゆっくり2回クリックして sqrt
および ship
をAltoエミュレーター側に転送します。あくまで、キー入力のエミュレートなので、Smalltalk-72 がプロンプトを出すまで待ってから次のコードを送るようにしてください。
disp clear
でウインドウ内のテキストを消せます。それに続けて@ home penup goto 200 100 pendn. ship 10
を実行して ship を描いてみましょう。
あれれぇ?(←わざとらしい) 引き延ばしたゼムクリップのような図形が描かれてしました^^;
念のため、Smalltalk-71 のコードを Logo コードと見做して Logo 処理系 で実行してみましたが、やはり同じ絵が描かれるので、変換ミスということではなさそうです。
さて困ったぞ…
Smalltalk-72で遊ぶOOPの原点:Smalltalk-71について
アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。
今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームを Smalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita
謎言語「Smalltalk-71」
実際に動作する Smalltalk処理系は 1972年の9月ごろに BASICで書かれた、のちに「Smalltalk-72」と呼ばれることになる言語の初期のバージョンが最初ですが、それ以前にも「Smalltalk-71」という、同じくアラン・ケイ氏考案の、これまたまったく別の言語がありました。かなり具体的な仕様まで考えられていたようで、たとえば構想段階の「ダイナブック」を紹介した「A Persona Computer for Children of All Ages(あらゆる年齢の『子供たち』のための パーソナルコンピュータ)」にも、今回扱うローリングストーン誌同様にコードを例として入れようとしていたようです。(このときは結局、ダニエル・G ・ボブロー氏の助言により取り除かれました。)
はずかしながらこのSmalltalk-71という言語については、カール・ヒューイットのPlannerという言語などからの影響で、文法を自由に定義できる機能を持っていたり、また、アラン・ケイ自身が1966年頃にアイヴァン・サザーランドのSketchpadやダールらのSimulaⅠ(この時点ではまだクラスやオブジェクトという用語は使われていなかった)のソースコードを読んでいてひらめいたオブジェクトやメッセージングの概念を(少なくともコンテキストレベルでは)持っていた…という程度の知識は「The Early History of Smalltalk(Smalltalkの初期の歴史)」から得てはいたものの、正直、ちんぷんかんぷんな状態でした^^;
「Smalltalkの初期の歴史」に掲載された Smalltalk-71 で、and演算、factorialやis-member-ofといった関数、Lisp の CONSセルのようなデータ構造、そして、ロボットの動作(!?)を実装した例がこちらになります。
一方で、呼び出し時のコンテキスト(スタックフレーム)はこんな構造を想定していたようです。
フィールド名 | 説明 |
---|---|
グローバル (GLOBAL) | パラメータ値の環境 |
送信者 (SENDER) | メッセージの送信者 |
受信者 (RECEIVER) | メッセージの受信者 |
返信スタイル (REPLY-STYLE) | 返信の方法(例:待機、分岐等) |
状態 (STATUS) | メッセージの進行状況 |
返信 (REPLY) | 最終的な結果(存在する場合) |
操作セレクタ (OPERATION SELECTOR) | 受信者に関連した操作 |
パラメータ数 (# OF PARAMETERS) | パラメータの数 |
P1, ..., Pn | 各パラメータ |
メッセージやセレクタといった、のちの Smalltalk でよく見かける用語も登場するものの、実際にそれらが、たとえば前述のようなコード上のどんな記述にどのように対応するのかははっきりと理解できないままなおざりにしてしまっていました。
では改めて、今回の Spacewar のコードの全体を眺めてみましょう。
まず、前述「Smalltalkの初期の歴史」版とは明らかに違う別の文法が用いられていることがわかります。「初期の歴史」版のようなにみられる is self のような明示的な記述は見当たりません。しかし、一部に :self という変数名が使われていることから、クラス定義を意識した部分もありつつ、たんなる関数の定義のセクションもある、そんな感じにも見て取れます。
この謎仕様の言語とそれで書かれたコード、なかなか手ごわそうです^^;
(宇宙船を描く(失敗編)へ続く)
Smalltalk-72で遊ぶOOPの原点:伝説のローリングストーン誌「スペースウォー」の記事
アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita
本企画のきっかけ
1972年12月7日号のローリングストーン誌の「Spacewar」という記事に絡めては、そこでPARCの自由な研究風景が紹介されたため、官僚的なゼロックス本社で大問題になった…という知る人ぞ知る歴史的な事件があったわけですが、その記事の最後にアラン・ケイ氏が、Logoに似た部分を持つごく初期のSmalltalkらしき言語で書いたスペースウォー・ゲームのシンプルな実装のコードが掲載されていることを、ハトダンナさんが教えてくださいました。
そして最後には「もしコンピュータを手に入れたら自分のスペースウォーを書けるよ」とありケイによるコードが載っています。これはSmalltalk-72…じゃなくてLogoでしょうか? pic.twitter.com/1ID3di8hDQ
— ハトダンナ(💻) (@pazworld_co) 2023年11月22日
ぱっと見た感じで私は、Smalltalk-72のごく初期のバージョンかと早とちりしてしまったのですが、その後、アラン・ケイ氏と親しい大島芳樹さんが、なんとご本人から「Smalltalk-71の考案中だった文法のひとつ」という貴重な情報を得てくださったのです!
本人に聞いてみました。
— よしき (@yoshikiohshima) 2023年11月23日
This was one of the syntaxes I was toying with for Smalltalk-71. The Rolling Stone article originally came out in Nov 1972, so it was after "the bet" and right before Dan finished the first version of what became Smalltalk-72.
だそうです。
これをダン・インガルス氏が自らの Lively-Web(Webブラウザ内で動作するGUIフレームワークおよびWebアプリ構築環境)上に復活させたSmalltalk-72(個人的に「May30バージョン」と呼んでいます)に書き直して実際に動かしてみたい!…というのが、本シリーズでの挑戦です。ただ、現時点ではまだ元のコードを読むのに四苦八苦している状態なので、あえなく失敗したらごめんなさい^^;
オリジナルのダン・インガルス版に少し手を加えてマウスクリックやナンチャッテクロック取得を可能にした私的なコピーを使用したいと思います。
このエミュレーターの「Snippets1」「Snippets2」「Snippets3」ボタンが追加されたバージョンでは、各ボタンを押して開くウインドウ状のウィジェット内で書いたSmalltalk-72のコードを、エミュレーター側のキー入力として転送する機能が付加されています。具体的には、各ウインドウ内の記述を模して、Smalltalk-72のプロンプトのAltoを模したコンピュータアイコンと do-it を表す「!」マークで括られたコードをタイプして記述するかコピペして貼り付けた後、そのままAltoマークの直後で(ダブルクリックにならないように)ゆっくり2回クリックすると、左側のAltoのエミュレータ画面にコードを転送することができます。
おそらく「The Evolution of Smalltalk: from Smalltalk-72 through Squeak(Smalltalkの進化:Smalltalk-72からSqueakまで)」で執筆のための拡張だったろうと想像しますが、ユーザー体験としての貧弱なSmalltalk-72のターミナル(?)に直接ちょっと長めのコードを書くとなるともはや苦行でしかなかったので、これはとても便利な機能でたいへん重宝しています。
デフォルトのままだとターミナルが小さすぎるので、Snippet3 の冒頭にあるコードをこの方法で転送・実行し、ウインドウを移動・拡大してみましょう。
さあ、準備は整いました。…が、まずは肝心のSmalltalk-71のコードを読み解くところから始めないとですね^^;
(Smalltalk-71についてへ続く)
Pythonが速度改善に本気出すと聞いたので恒例のたらい回しベンチをとってみたら、RubyがYJITですごく速くなっていて驚いた話
2022-09-09改訂: gcc バージョンが古すぎたのと、C が内部計測でなかった点を改め計測しなおしました。結果、Rust は C より速くはなくなりました。紛らわしいことで、ごめんなさい。また、gcc のバージョンアップに伴い、Python および Ruby についてはビルドと計測をしなおしたので、これらも少し速い値に変わっています。この点もどうぞあしからず。
2022-09-10追記:ご要望のあった Python numba.njit 使用時と Go の結果を追加しました。PHP は JIT 有効化が面倒だったので断念しました^^;
2022-09-10追記2:C の計測で clock() を使うのはフェアではないという指摘がありましたので、念のため clock_gettime() を使用したコードに差し替えました。結果に大きな差はありません。
2022-09-10追記3:PHP の JIT が 有効化されない理由と回避方法 が分かったので、計測して追加しました。
2022-09-11追記:PyPy の結果を追加しました。
2022-09-12追記:Common Lisp の結果を追加しました。
2022-09-13追記:Java(HotSpot、GraalVM)と C# 、GraalVM をインストールしたついでに TruffleSqueak、GraalPython、TruffleRuby の結果を追加しました。Java HotSpotVM が C より速くなってしまったのがナゾです。^^; ※その後、使用できるコア数を1に限ったり、引数をコマンドラインから与える、n=8 でも比較する等の検証をしましたが再現し、フェアさに欠ける点は特に見つけられませんでした。
2022-09-13追記2:PHP.ini の設定をしくじっていた(opcache.jit_buffer_size=128MBと最後に余計な"B"を付けてしまっていた^^;)ようで JIT が効いていませんでした。ごめんなさい。ご指摘いたみいります。ということで、PHP は「JITあり」の結果を改めて追加しました。
2022-09-13追記3:Ruby MJIT の結果を追加しました。
2022-09-16追記:お詳しい方からご指摘をいただき Common Lisp の最適化時の結果を更新しました。手元の環境では C より速く、Java を軽く制して今のところ最速です。Lisp 怖い (いろんな意味で^^;)。
2022-09-17追記:C# 最適化後と Swift の結果を追加しました。
2022-12-29追記:F# の結果を追加しました。
竹内関数 (今回はyを返す正式版) tarai(14, 7, 0) にかかる時間を、手元の環境(Intel Core i9-9880H @ 2.30GHz、Win10 64-bit WSL環境)で計測してみました。
- またまた久しぶりに竹内関数で JavaScript、Python、Ruby、Scheme と Smalltalk とを戦わせてみる
- 久しぶりに竹内関数で JavaScript、Python、Ruby、Scheme と Smalltalk とを戦わせてみる
Smalltalk は Squeak と Pharo の Win版を使いました。今回もいったんは従来通りブロック(無名関数)版の実装で計測したのですが、タイトルの通り、YJITありのRubyに思いのほか肉薄され、もはや余裕がなくなったので、少しでも差を付けられるようにメソッド版でも試しました。😅
Cの結果がたまたま1秒ちょうどだったので、今回は結果の数値がそのまま「Cの何倍か」になります。Ruby YJIT をビルドするついでに Rust をインストールしたので Rust でも計測してみました(コードは Rustでたらいまわし よりお借りして改変しました)。「Cより速い」つながり(?)で Julia でも試してみましたが、どうやら竹内関数ではそこまで速くはならないみたいです(でも十分速い!)。
言語 | 処理系 | 結果 | 対C比 | 前回(ニセ竹内関数)の対C比 |
C | gcc 9.4.0 | 0.735 sec | 1.00倍 | — |
Smalltalk (メソッド) | Squeak 6.0 | 2.55 sec | 3.47倍 | — |
TruffleSqueak 22.2.0 | 5.59 sec | 7.61倍 | — | |
Pharo 10.0.0 | 2.45 sec | 3.33倍 | — | |
Smalltalk (ブロック) | Squeak 6.0 | 3.87 sec | 5.27倍 | 4.45倍 (Squeak 5.3) |
TruffleSqueak 22.2.0 | 21.1 sec | 28.7倍 | — | |
Pharo 10.0.0 | 3.62 sec | 4.93倍 | 4.81倍 (Pharo 8.0.0) | |
Python | Python 3.11.0rc1 | 29.3 sec | 39.9倍 | — |
Python 3.10.7 | 53.0 sec | 72.1倍 | 74.6倍 (3.10.0b3+) | |
Python 3.10.7 (numba.njit使用時) | 3.58 sec | 4,87倍 | — | |
PyPy 3.9-7.3.9 | 9.19 sec | 12.5倍 | — | |
GraalPython 3.8.5 | 7.20 sec | 9.80倍 | — | |
Ruby | Ruby 3.2.0dev (YJIT) | 4.08 sec | 5.55倍 | — |
Ruby 3.2.0dev (MJIT) | 5.47 sec | 7.44倍 | — | |
Ruby 3.2.0dev (JITなし) | 17.7 sec | 24.1倍 | 22.2倍 (3.1.0dev) | |
truffleruby 22.2.0 | 5.14 sec | 6.99倍 | — | |
JavaScript | Node.js 17.9.1 | 2.14 sec | 2.91倍 | 2.84倍 (17.9.1) |
Julia | Julia 1.8.0 | 1.18 sec | 1.61倍 | — |
Rust | rustc 1.63.0 | 0.790 sec | 1.07倍 | — |
Go | go1.19 | 0.849 sec | 1.16倍 | — |
PHP | PHP 8.1.0 (JITあり) | 4.47 sec | 6.08倍 | — |
PHP 8.1.0 (JITなし) | 11.3 sec | 15.4倍 | — | |
Common Lisp | SBCL 1.4.5 | 2.61 sec | 3.55倍 | — |
SBCL 1.4.5 (最適化あり) | 0.590 sec | 0.803倍 | — | |
Java | OpenJDK 18.0.2.1 | 0.655 sec | 0.891倍 | — |
OpenJDK 17.0.4 GraalVM CE 22.2.0 | 0.884 sec | 1.20倍 | — | |
C# | .Net 6.0.400 | 1.81 sec | 2.46倍 | — |
.Net 6.0.400 (最適化あり) | 1.05 sec | 1.43倍 | — | |
F# | .Net 6.0.400 | 1.27 sec | 1.73倍 | — |
.Net 6.0.400 (最適化あり) | 0.983 sec | 1.34倍 | — | |
Swift | swiftc 5.7 | 5.58 sec | 7.59倍 | — |
swiftc 5.7 (最適化あり) | 0.780 sec | 1.06倍 | — |
x が y より大きくないときに z ではなく y を返すように変えたのと、竹内先生のコードに倣って if 内の順番を変えた以外は前回とほぼ一緒です。ニセ(…と連呼すると語弊がありますが「マッカーシー版」)と違って正式な竹内関数 tarai(2n, n, 0) では n=10 だと無理っぽそうだったので、n=7 で計算させました。
Pythonは伸びしろがいっぱいあるので「4年後に5倍の速度」は大丈夫そうですね!
Smalltalk (メソッド版)
「Tarai class >>」はクラスブラウザなどを使って定義した「Tarai」クラスのクラスメソッドに x:y:z: メソッドを定義することを意味します。
Tarai class >> x: x y: y z: z ^ x > y ifTrue: [ Tarai x: (Tarai x: x-1 y: y z: z) y: (Tarai x: y-1 y: z z: x) z: (Tarai x: z-1 y: x z: y)] ifFalse: [y]
| ans | (Time millisecondsToRun: [ans := Tarai x: 14 y: 7 z: 0]) -> ans
Smalltalk (ブロック版)
| tarai ans | tarai := nil. tarai := [:x :y :z | x > y ifTrue: [ 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)] ifFalse: [y] ]. (Time millisecondsToRun: [ans := tarai value: 14 value: 7 value: 0]) -> ans
Cの結果と使用したコード
$ gcc --version gcc (Ubuntu 9.4.0-1ubuntu1~18.04) 9.4.0 ... $ gcc -O3 tarai.c -o tarai_O3 $ ./tarai_O3 0.734794 14 $ cat tarai.c #include <stdio.h> #include <time.h> int tarai(int x, int y, int z){ if (x > y){ return tarai( tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y) ); } else { return y; } } int main(void){ struct timespec time1, time2; float delta; clock_gettime(CLOCK_REALTIME, &time1); int ans = tarai(14, 7, 0); clock_gettime(CLOCK_REALTIME, &time2); delta = time2.tv_sec - time1.tv_sec + (float)(time2.tv_nsec - time1.tv_nsec) / 1000000000; printf("%f %d\n", delta, ans); return 0; }
Pythonの結果と使用したコード
$ python -V; python tarai.py Python 3.10.7 53.01142239570618 14 $ python -V; python tarai.pyPython 3.11.0rc1 29.337019681930542 14 $ cat tarai.py from time import time def tarai(x, y, z): if x > y: return tarai( tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y) ) else: return y start = time() ans = tarai(14, 7, 0) print( time() - start, ans )
Python numba.njit 使用時の結果とコード
$ pip list Package Version ---------- ------- llvmlite 0.39.1 numba 0.56.2 numpy 1.23.2 pip 22.2.2 setuptools 59.8.0 $ python -V; python tarai-njit.py Python 3.10.7 3.5813117027282715 14 $ cat tarai-njit.py from time import time from numba import njit, i2 @njit(i2(i2,i2,i2)) def tarai(x, y, z): if x > y: return tarai( tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y) ) else: return y start = time() ans = tarai(14, 7, 0) print( time() - start, ans )
PyPyのバージョンと結果
$ pypy -V; pypy tarai.py Python 3.9.12 (05fbe3aa5b0845e6c37239768aa455451aa5faba, Mar 29 2022, 08:15:34) [PyPy 7.3.9 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)] 9.192949771881104 14
GraalPython のバージョンと結果
$ graalpython -V; graalpython tarai.py GraalVM Python 3.8.5 (GraalVM CE Native 22.2.0) 7.203999996185303 14
Rubyの結果と使用したコード
$ ruby -v tarai.rb ruby 3.2.0dev (2022-09-08T15:46:21Z master 6d93644ba1) [x86_64-linux] 14 17.7318423 $ ruby --yjit -v tarai.rb ruby 3.2.0dev (2022-09-08T15:46:21Z master 6d93644ba1) +YJIT [x86_64-linux] 14 4.084934 $ ruby -v --mjit tarai7.rb ruby 3.2.0dev (2022-09-08T15:46:21Z master 6d93644ba1) +MJIT [x86_64-linux] 14 5.468897 $ cat tarai.rb def tak(x, y, z) if x > y then tak(tak(x-1, y, z), tak(y-1, z, x), tak(z-1, x, y)) else y end end start = Time.now puts tak(14, 7, 0) puts Time.now - start
TruffleRuby のバージョンと結果
$ ruby -v tarai.rb truffleruby 22.2.0, like ruby 3.0.3, GraalVM CE Native [x86_64-linux] 14 5.1394269999999995
JavaScript (Node.js) の結果と使用したコード
$ node -v; node tarai.js v17.9.1 2136 14 $ cat tarai.js function tarai(x, y, z) { if(x > y) { return tarai( tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y) ); } else { return y; } } var start = (new Date()).getTime(); var ans = tarai(14, 7, 0); console.log((new Date()).getTime() - start, ans)
Juliaの結果と使用したコード
$ julia -v; julia tarai.jl julia version 1.8.0 1.184309 seconds 14 $ cat tarai.jl tak(x, y, z) = x > y ? tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y)) : y @time ans = tak(14, 7, 0) println(ans)
Rustの結果と使用したコード
$ rustc -V rustc 1.63.0 (4b91a6ea7 2022-08-08) $ rustc -C opt-level=3 -C debug_assertions=no tarai.rs $ ./tarai 14 0.7899205 $ cat tarai.rs use std::time::Instant; fn tarai(x: i32, y: i32, z: i32) -> i32 { if x > y { tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y)) } else { y } } fn main() { let start_tarai = Instant::now(); println!("{}", tarai(14, 7, 0)); let end_tarai = start_tarai.elapsed(); let tarai_sec = end_tarai.as_secs() as f64 + end_tarai.subsec_nanos() as f64 / 1000_000_000.0; println!("{}", tarai_sec); }
Goの結果と使用したコード
$ go version go version go1.19 linux/amd64 $ go run tarai.go 849 14 $ cat tarai.go package main import ( "fmt" "time" ) func tarai(x int, y int, z int) int { if x > y { return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y)) } else { return y } } func main() { start := time.Now() ans := tarai(14, 7, 0) fmt.Printf("%v %v\n", time.Since(start).Milliseconds(), ans) }
PHPの結果と使用したコード
$ php -i | grep opcache opcache.enable => On => On opcache.enable_cli => On => On opcache.jit => tracing => tracing opcache.jit_buffer_size => 128M => 128M opcache.revalidate_freq => 60 => 60 ... $ php -v; php tarai.php PHP 8.1.9 (cli) (built: Sep 10 2022 22:10:58) (NTS) Copyright (c) The PHP Group Zend Engine v4.1.9, Copyright (c) Zend Technologies with Zend OPcache v8.1.9, Copyright (c), by Zend Technologies 4.473552942276 14 $ php -i | grep opcache opcache.jit_buffer_size => 0 => 0 ... $ php tarai.php 11.325684070587 14 $ cat tarai.php <?php function tarai(int $x, int $y, int $z) : int { if ($x > $y) { return tarai(tarai($x - 1, $y, $z), tarai($y - 1, $z, $x), tarai($z - 1, $x, $y)); } else { return $y; } } $start = microtime(true); $ans = tarai(14, 7, 0); $time = microtime(true) - $start; echo "{$time} {$ans}\n";
Common Lisp (最適化なし)の結果と使用したコード
$ sbcl --version; sbcl --script tarai.lisp SBCL 1.4.5.debian Evaluation took: 2.608 seconds of real time 2.607781 seconds of total run time (2.606888 user, 0.000893 system) 100.00% CPU 6,008,334,038 processor cycles 0 bytes consed 14 $ cat tarai.lisp (defun tarai (x y z) (if (> x y) (tarai (tarai (1- x) y z) (tarai (1- y) z x) (tarai (1- z) x y)) y ) ) (defvar *ans*) (time (setq *ans* (tarai 14 7 0))) (princ *ans*) (terpri)
Common Lisp (最適化あり)の結果と使用した「https://g000001.cddddr.org/3872263095」でご提示いただいたコード
$ sbcl --script tarai7-g1.lisp Evaluation took: 0.590 seconds of real time 0.590533 seconds of total run time (0.590533 user, 0.000000 system) 100.17% CPU 1,360,580,830 processor cycles 0 bytes consed 14 $ cat tarai-g1.lisp (defun tarai (x y z) (declare (optimize (speed 3) (debug 0) (safety 0))) (labels ((tarai (x y z) (declare (fixnum x y z)) (the fixnum (if (> x y) (tarai (tarai (1- x) y z) (tarai (1- y) z x) (tarai (1- z) x y)) y)))) (declare (inline tarai)) (tarai x y z))) (compile 'tarai) (princ (time (tarai 14 7 0))) (terpri)
Java HotSpotVMの結果と使用したコード
$ java -version; javac -version openjdk version "18.0.2.1" 2022-08-18 OpenJDK Runtime Environment (build 18.0.2.1+1-1) OpenJDK 64-Bit Server VM (build 18.0.2.1+1-1, mixed mode, sharing) javac 18.0.2.1 $ javac tarai.java $ java Tarai 655 14 $ cat tarai.java class Tarai { static private int tarai(int x, int y, int z) { if (x > y) { return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y)); } else { return y; } } static public void main(String args[]) { long start = System.currentTimeMillis(); int ans = tarai(14, 7, 0); System.out.println(System.currentTimeMillis() - start); System.out.println(ans); } }
Java GraalVMのバージョンと結果
$ java -version; javac -version openjdk version "17.0.4" 2022-07-19 OpenJDK Runtime Environment GraalVM CE 22.2.0 (build 17.0.4+8-jvmci-22.2-b06) OpenJDK 64-Bit Server VM GraalVM CE 22.2.0 (build 17.0.4+8-jvmci-22.2-b06, mixed mode, sharing) javac 17.0.4 $ javac tarai.java $ java Tarai 884 14
C#の結果と使用したコード
$ dotnet --version 6.0.400 $ dotnet new console -o Tarai_cs $ cat Program.cs using System.Diagnostics; class Program { static void Main() { var sw = new Stopwatch(); sw.Start(); var ans = Tarai(14, 7, 0); sw.Stop(); Console.WriteLine("" + sw.ElapsedMilliseconds + " " + ans); } static int Tarai(int x, int y, int z) { if (x > y) { return Tarai( Tarai(x - 1, y, z), Tarai(y - 1, z, x), Tarai(z - 1, x, y)); } else { return y; } } } $ dotnet run 1809 14 $ dotnet run --configuration Release 1048 14
F#の結果と使用したコード
$ dotnet --version 6.0.400 $ dotnet new console -lang F# -o Tarai_fs $ cat Program.fs let rec tarai x y z = if x > y then tarai (tarai (x - 1) y z) (tarai (y - 1) z x) (tarai (z - 1) x y) else y let stopWatch = System.Diagnostics.Stopwatch.StartNew() let ans = tarai 14 7 0 stopWatch.Stop() printfn "%f %d" stopWatch.Elapsed.TotalMilliseconds ans $ dotnet run 1272.238400 14 $ dotnet run --configuration Release 983.585300 14
Swiftの結果と使用したコード
$ swift -version Swift version 5.7 (swift-5.7-RELEASE) Target: x86_64-unknown-linux-gnu $ swiftc tarai.swift && ./tarai 14 5.5811779499053955 $ swiftc -Ounchecked tarai.swift && ./tarai 14 0.7802159786224365 $ cat tarai.swift import Foundation import CoreFoundation func tarai(_ x: Int, _ y: Int, _ z: Int) -> Int { if x > y { return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y)) } else { return y } } let start = CFAbsoluteTimeGetCurrent() let ans = tarai(14, 7, 0) print("\(ans) \(CFAbsoluteTimeGetCurrent() - start)")
またまた久しぶりに竹内関数で JavaScript、Python、Ruby、Scheme と Smalltalk とを戦わせてみる
竹内関数 (正確には“ニセ”竹内関数) tak(20, 10, 0) にかかる時間を、手元の環境(Intel Core i9-9880H @ 2.30GHz、Win10 64-bit WSL環境)で計測してみました。前回からちょうど(?)7年ですね^^;
- 前回 久しぶりに竹内関数で JavaScript、Python、Ruby、Scheme と Smalltalk とを戦わせてみる
- 前々回? ちまた(っても Squeak 界隈限定)で流行りの Coke で遊んでみました
Smalltalk は Squeak は Win版をそのまま、Pharo は次のページを参考に WSL にインストールして動かしてみました。
言語 | 処理系 | 結果 | 対C比 | 参考:2014年時点 |
Smalltalk | Squeak 5.3 | 0.596 sec | 4.45倍 | 6.12倍 (Squeak 4.5) |
Pharo 8.0.0 | 0.644 sec | 4.81倍 | — | |
GNU Smalltalk 3.2.5 | 7.97 sec | 59.5倍 | 65.0倍 (3.1) | |
Python | Python 3.10.0b3+ | 10.0 sec | 74.6倍 | 52.53倍 (3.2.5) |
Ruby | Ruby 3.1.0dev | 2.97 sec | 22.2倍 | 20.8倍 (2.2.0dev) |
Scheme | Gauche 0.9.10 | 2.44 sec | 18.2倍 | 15.1倍 (0.9.5_pre1) |
JavaScript | Node.js 16.3.0 | 0.381 sec | 2.84倍 | 2.23倍 (0.10.31) |
C | gcc 7.5.0 | 0.134 sec | 1.00倍 | 1.00倍 (4.8.3) |
対C比では7年前とあまり代わり映えしませんが、Rubyが速くなっていないのがちょっと意外ですね。
コードは以前と一緒です。
| tak res | tak := nil. tak := [:x :y :z | x <= y ifTrue: [z] ifFalse: [ tak value: (tak value: x-1 value: y value: z) value: (tak value: y-1 value: z value: x) value: (tak value: z-1 value: x value: y) ] ]. (Time millisecondsToRun: [res := tak value: 20 value: 10 value: 0]) -> res
Squeak は起動後 Tools → Workspace で、Pharo は ./pharo-ui で起動後、Tools → Playground でプレイグラウンドを呼び出し、コードを貼り付けてから全選択、右クリックメニューや ctrl + p などで print it すると実行と結果を表示できます。
GNU Smalltalk でも動きますが、最後の式を括弧でくくって printNl しないと結果を出力できません。
$ gst -v; gst tak.st GNU Smalltalk version 3.2.5 ... 7976->1 $ cat tak.st | tak res | tak := nil. tak := [:x :y :z | x <= y ifTrue: [z] ifFalse: [ tak value: (tak value: x-1 value: y value: z) value: (tak value: y-1 value: z value: x) value: (tak value: z-1 value: x value: y) ] ]. ((Time millisecondsToRun: [res := tak value: 20 value: 10 value: 0]) -> res) printNl
その他の言語のコードと出力
$ python -V; python tak.py Python 3.10.0b3+ 10.04528284072876 1 $ cat tak.py from time import time def tak(x, y, z): if x <= y: return z return tak( tak(x-1, y, z), tak(y-1, z, x), tak(z-1, x, y) ) start = time() res = tak(20, 10, 0) print( time() - start, res )
$ ruby -v tak.rb ruby 3.1.0dev (2021-06-23T06:14:21Z master 69ce9e4187) [x86_64-linux] 1 2.9718293 $ cat tak.rb def tak(x, y, z) if x <= y then z else tak(tak(x-1, y, z), tak(y-1, z, x), tak(z-1, x, y)) end end start = Time.now puts tak(20, 10, 0) puts Time.now - start
$ gosh -V; gosh tak.scm Gauche scheme shell, version 0.9.10 [utf-8,pthreads], x86_64-pc-linux-gnu ... ;(time (display (tak 20 10 0))) ; real 2.441 ; user 2.440 ; sys 0.000 1 $ cat tak.scm (define (tak x y z) (if (<= x y) z (tak (tak (- x 1) y z) (tak (- y 1) z x) (tak (- z 1) x y)))) (time (display (tak 20 10 0)))
$ node -v; node tak.js v16.3.0 381 1 $ cat tak.js function tak(x, y, z){ if(x <= y){ return z; } return tak( tak(x-1, y, z), tak(y-1, z, x), tak(z-1, x, y) ); } var start = (new Date()).getTime(); var res = tak(20, 10, 0); console.log((new Date()).getTime() - start, res)
$ gcc --version gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 ... $ cat tak.c #include <stdio.h> int tak(int x, int y, int z){ if (x <= y){ return z; } else { return tak( tak(x-1, y, z), tak(y-1, z, x), tak(z-1, x, y) ); } } int main(void){ printf("%d\n", tak(20, 10, 0)); return 0; } $ gcc -O3 tak.c -o tak_O3 $ time ./tak_O3 1 real 0m0.134s user 0m0.134s sys 0m0.000s
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 率いるプロジェクトだけに。