nashornのbenchmarkについて

rhinoの20倍のスコアを出すというbenchmark云々についてソースコードを見つけたので書いておく。
前提としてこのbenchmarkはmicroな話であってそれはoracleもはじめから言っている。
そして、この早いというのは完全にカンファレンスでよくあるネタなのであしからず。ただ、macroな話になると実際にはそれこそ、逆にrhinoの方が20倍早いくらいな方なので気になったので調べてみた。

まずはじめに、このmicrobenchmarkは[%src_dir%/test/examples]にある。

で、何故macroだと20倍遅くてmicroだと20倍早いのかそのカラクリは予想通りアーリーバインディングしているからだ。単純にそれがmicroな世界では利いている。rhinoではjsの型はjavaから見ればラッパークラスなのでアーリーバインディングしなければボクシングを必要とするのでVMの低レベルな部分から見れば無駄が多い。

この「VMの低レベルな部分から見れば」というところが重要で、これこそがmacroでは遅くてmicroでは早い直接の理由。そもそもJVM言語にとってVMの低レベルな部分で無駄が多いと色々とても面倒くさい。そのためにinvokedynamicが追加された、そうすると面倒な部分をAPIとVMに丸投げ出来るからだ。そして、microな世界で早いということは(丸投げしている)それそのものには大したオーバーヘッドが生じないということ。

それはつまり、JVM言語の実装を大したオーバーヘッドもなしに楽できるということなのでJVM言語の実装者は皆ハッピーになれる。invokedynamicに関連付けられる型は静的な型なので最適化しやすい。だけどinvokedynamicは速さのためにあるわけではなく型の扱いやら動的ディスパッチを丸投げする仕組みなのでJVM言語のような用途以外で使っても逆に遅くなるはず。

nashornの話に戻ると「じゃあ、V8は暗黙のアーリーバインディングしてるから早い」という話を聞いたことがあるかもしれないが、確かにアーリーバインディングするとmicroな世界で早くなるし型が既に決まっているので都合がいい。しかし、V8のような最適化手法は型をアーリーバインディング出来るから早いというより、スクリプトを事前評価できる部分をそのタイミングでコンパイルしてキャッシュしまくっているからそれがかなり利いている。どちらかと言うとこれが早い。

事前評価なのでキャッシュが無効になるとまたコンパイルし直さなければならない。だから最終的にそれに失敗すると遅くなる。これが、最近はよく知られているV8が脱最適化した遅い状態だ。なのでコンパイルされた部分が早くてもそれ以外が遅いとmacroな世界では遅くなる。

nashornの場合はやってるのはただのアーリーバインディング+java byte codeへのコンパイルなのでこのbyte code、つまるところはこの実装が遅ければmacroな世界では遅いということ。




要するにnashornは、やっぱりinvokedynamicの宣伝とサーバーで使いたいのと、もう一つ、ソースコードを外部のコミュニティではなくopenjdkで管理したいため(これがけっこう切実な問題)。それとサーバーサイドのnode.jarならば、一度nashornが起動すれば十分に働きっぱなしなのでマシン語で走っている時間が多くなる。

ただし、nashornの実装自体が遅いのでmacroな世界、実際の使用でどれくらい期待できるかはやってみないと分からない。これなんかはmicrobenchmarkでCとjavaを比べてjavaの方が遅いと結論付けるのと同じこと。



microな世界でもただのtarai関数ですらrhinoの方が早い(firefoxで試してもJaegermonkeyが仕事してないのかrhinoの方が早い)。まあ、rhinoもnashornも速度に関してはもっと改善の余地があるのでまだ早くなれると思う。
nashornは苦手な部分がハッキリしてるし、rhinoに至ってはバイトコードジェネレータが無駄なコード履いてる。rhinoランタイムの方が手書きでガリガリやってるからある程度チャラになってるんだけど四則計算と変数の代入すらランタイムのAPI呼び出しになってるのはさすがに無駄がある。
nashornは純粋な数値計算が大部分を占めるならmacroな世界でもかなり早いんじゃないだろうか、科学計算とか如何?
というかこのmicrobenchmarkはrhinoとnashornがコンパイルモードだからこれくらい早いんであって混載モードのブラウザの実装とかだったらもっと遅いよね。

未来のjavascriptでやってはいけないこと色々

class

classそのものが悪いといっているわけではないので安心してほしい。(ただし、継承でできる事は移譲でできる)しかし、classが出来るからと言って空実装のメソッドを定義して抽象クラスだのインターフェースだのとするのは全くもって無意味です。

なぜなら、特定のメソッドだけを公開して要件を満たすようにするというのはオブジェクト指向ではなく抽象データ型のパラダイムだからです。(C++のオブジェクト指向は抽象データ型のスーパーセットであるためそのようになっているだけ、抽象データ型===オブジェクト指向というわけではない)
むしろ、コンテキストによってレシーバが受け取れるメッセージを見えなくするという行為は、オブジェクトはメッセージを受け取れるというオブジェクト指向に反します。

コンテキストに応じて公開か非公開というパラダイムはオブジェクト指向のドメインではなく抽象データ型のドメインなので抽象データ型のパラダイムを持たない(公開/非公開という属性は持ってないですよね)javascriptでは通用しない考え方です。

しかし、抽象データ型の考え方を持ち込んだほうが良い場合もあります。その様にオブジェクトを特定のドメインに適応させて扱うにはMOPを用います。MOPではオブジェクトの挙動そのものを変更できるので言ってしまえばなんでも出来ます。重要なのは何をMOPで行うべきかを考察する事です。

モンキーパッチ

MOPではなんでも変えることが出来ますが、出来るからと言ってなんでも変えるべきではありません。また、見た目を変えずに挙動を変えるモンキーパッチもやるべきではありません。開発者はモンキーパッチが喧嘩しないように考慮することは不可能ですし、利用者からは全く不透明です。また、プロトタイプ汚染もこれに含みます。
これらはクラス・ベースでクラスの挙動を変えることと同義です。即ちそれと同じ問題をはらんでいます。それはつまり、パッチによる変更はある時点で他から変更されていないという前提が成り立たないことがクラスの動的な変更と同じことだといえます。
静的言語脳の人は必ずしもクラスの挙動が静的な情報に依存するわけではないことを留意しておいて下さい。(ただし、色々な都合でそれに制限されることはあります)

プロトタイプの変更

既存のオブジェクトに対して__proto__プロパティへの代入ができなくてもObject.create()を使えば[[Prototype]]を変更できます。これはプロトタイプそのものの変更です。これもクラス・ベースでクラスの挙動を変えることと同義です。

これら2つを阻止するにはconstと属性を用いることが出来ます。というかそうすべきです。でなければオブジェクトやプロトタイプは常に破壊される(その脅威にさらされる)ことになるでしょう。

javascriptはnominal typeな言語ではない

nominal typeな言語ではオブジェクトと型の違いが曖昧です。名前しか見ていないからです。javascriptはstructural typeな言語です。構造にこそ意味があります。
多相型とか共変型とか悩むのは時間の無駄です。構造さえ一致していればいいのです。元からこういう特性ですので、これをわざわざ特別にダックとか呼ぶ必然性もありません。こういう仕組みは昔からありますし、そういう言語は始めからそういう仕組みです。一番良いのはそれに慣れることです。

この様な仕組みがあることは非常に強力です。型に関係性がないからといって別のオブジェクトが、同じ要件のメソッドやデータを持っていないとはいえません。これはC++風のクラス・ベース的な考え方しか出来ない場合に注意が必要です。

C++は低レベル寄りなためnominalなクラス・ベースですから仕方ありませんがstructural typeでは型には名前以上の意味があります。名前だけでは確保したメモリの長さ程度しか判りませんが構造に目を向ければオブジェクトが何を持っているか(何が出来るか)も分かります。

nominal typeでは満たすべき要件の情報は外部から与えられますが、こちらは外部にある満たすべき要件に名前をつけるイメージ。structural typeではオブジェクト自身が自分が満たせる要件の情報を持っていますが、こちらは自分が何を出来るかおおっぴらにしていて、これが出来る自分は○○という者だと宣言しているイメージと言ったところです。
○○がこれが出来ると主張しているからと言って××にはそれが出来ないとは限りません。(nominal typeでは型に関係性がなければそれは叶わない)なのでメッセージを主体とするアラン・ケイのオブジェクト指向とは相性がいい、逆に自分に何が出来るか隠そうとする抽象データ型(のスーパーセットであるC++のオブジェクト指向含む)の考え方とは相性が悪いと言えます。

別の振る舞いの中に共通した振る舞いがあれば多相ですし、扱うものが違っても同じ振る舞いを持っていれば共変です(整数型と実数型を考えてみれば自らが持っているデータは違っても四則計算をするメソッドは同じ振る舞い……といった様に分かりやすい)。
振る舞いというのは自分のスロットにある関数と言えます。javascriptでは自分に紐付いている関数もデータも区別せずプロパティと呼びます。つまるところ、どんなプロパティを持っているかというのが構造の言わんとするところです。型名同士の関係性は重要ではありません。

この様な型システムの違いをポピュラーなC系の言語の観点から見てnominal typeとstructural typeと言います。(多分、英語でググると色々見つかるはず)


traits object

javascriptでは関数のprototypeプロパティに参照されたオブジェクトがtraits objectとして振る舞うので意味が無い。(builtin objectの場合、そのコンストラクタのprototypeプロパティはprototype objectを指している)
traits objectがデフォルト実装を持ったインターフェースというのは必ずしもそうは言えない。ですが、インターフェースのように外部から与えられた要件を持つ必要があるという場合は存在します(ただし、インスタンス・ベースではtraits objectとは必ずデフォルトの振る舞いを持つものである。なぜなら逐一cloneを作る代わりにtraits objectを使うからで、それはinterfaceではなく具体的な振る舞いを持ったobjectである)。

その場合は関数のprototypeプロパティにオブジェクトを突っ込む。ただし、これだと(プロトタイプとtraits objectは別物であるにもかかわらず)プロトタイプを同じくするすべてのオブジェクトが影響を受けるので実際にはObject.defineProperties()するのが正解。

javascriptに限ってはObject.create()でcloneを作ると任意の移譲が発生(__proto__への代入)してしまうのでcloneを作るにはnewする必要がある。newならば移譲先はobjectプロトタイプオブジェクトである。実際にはnewするとprototypeプロパティを見に行ってしまうので純粋なcloneではない。純粋なcloneはプロパティを列挙してコピーするかまたは以下のようにする──。

// javascriptにcopyメッセージはない
// javascriptではコンストラクタを使う
function O(){
// do something with this
}
O.prototype = {/* traits properties */};
var copy1 = new O();
var copy2 = new O();
// それかcloneせずに逐一新たに作りなおす
// またはObject.create(Object.prototype, properties)
// とするがこれは冗長なだけである。
// さらにObject.create(new Object, properties)との
// 違いがわからないなら一から作ったほうが無難。
// "この場合"第一引数がprototype objectで第二引数がtraits objectである

var obj1 = {};
// do something

さて本題に戻ろう。ただのインスタンス・ベースではなくプロトタイプ・ベースにおけるtraits objectとは移譲先にぶち込まれたオブジェクトのことなのでクラス・ベースの言うtraitsとインスタンス・ベース/プロトタイプ・ベースのいうtraitsは意味が違うということ。

ただし、非常にややこしいことに一般的なプロトタイプ・ベースのtraits objectとjavascriptのtraits object(ひいてはprototype object)は別物。一般的なプロトタイプ・ベースのプロトタイプとはclone元のことを言う。そして、一般的なプロトタイプ・ベースのtraitsはcloneによって作られた新たなオブジェクトの移譲された共有される振る舞いを持つオブジェクトを言う。

つまり、javascript風に言えば一般的なプロトタイプ・ベースでは[[Prototype]]を差し替えている。javascriptではこれを禁止する代わりに関数のprototypeプロパティに参照されたオブジェクトを見にいっている(それによってtraits objectを実現する)。

魔法使いが__proto__への代入をしたがるのはそれがより一般的だからだが、しかし、それは非常に強力で危険を伴う行為なのでecma-262ではそれを嫌う(強力な魔法は詠唱者を汚染するものだ:-T)。

それがstandard objectの場合、一般的に言うプロトタイプは仕様によって定義されているのでそれをprototype objectという。この様なややこしさからかes6ではこの仕様で定義されたstandard objectのプロトタイプのことをordinary objectと呼ぶようになった。
一般的なプロトタイプ・ベースのcopyメッセージ+traits objectはjavascriptではコンストラクタによって改良・改善されている。コンストラクタではcopyより容易にテンプレート処理できる。



wikipediaの
http://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%88%E3%82%BF%E3%82%A4%E3%83%97%E3%83%99%E3%83%BC%E3%82%B9

クラスが存在しない世界ではテンプレート処理によるインスタンス化ができないため、新しいオブジェクトは完全に空の状態か、または他のオブジェクトの複製(クローン)によって作成される。プロトタイプベースでの継承は一般にこのクローンによる特性の引き継ぎを指す。

は嘘だから信じないように、そもそもプロトタイプしか持たない場合、継承ではなく移譲。移譲の一部は継承だが継承は移譲ではない。理由は『改めてプロトタイプベースとは?』に説明したとおりなのでそこ参照。プロトタイプベースがクラスを持たないとは限らない。これもそこ参照。


だけどどうしてもtraits objectを使いたい時はObject.defineProperties()する。
そうする理由はただ一つ、traits objectの影響が他のオブジェクトに影響を受けたくない時だけ(インスタンスプロパティにしたい)。
ただし、definePropertiesはプロパティの追加ではなくプロパティの属性の付与の側面が強いので純粋にプロパティの追加に使うものではない。純粋なプロパティの追加は副作用が許されているjavascriptでは[[Set]]、つまり(副作用が許されない言語では考えられないかもしれないが)単純に代入によって行う。



[[Set]]は他のプロトタイプ/インスタンスベースではスロットの追加を意味する。しかし、javascriptには固定されたスロットはない。代わりに属性を使う。


__proto__を変更するのはjavascriptではナンセンスなのでこれが妥協案。Object.defineProperties()ではインスタンスプロパティになるので既存が影響を受けないという改善点もある。(何よりその為のものなのでもちろん属性の付与も出来る)



そもそも素直にコンストラクタ使えという話


まあ、es3でもインスタンスにプロパティを一つ一つ追加する処理を関数化すればいいのだけど属性が扱えるという利点がないわけでもない。ただ、インスタンス・ベース的には一度作られたスロットは追加されないほうが肥大化しなくて済む。

関係のないインスタンスに関係のない関数を持たない

es5で追加されたObject.*関数群すべてのことです。GlobalのObjectプロパティはObjectオブジェクトのコンストラクタなのでコンストラクタがオブジェクトのメタな操作を持つのはふさわしくありません。なぜならコンストラクタということはそれは立派な関数であるし、メタ操作というのはオブジェクトのための特別な操作であるからです。

オブジェクトのメタ操作を行う関数群を放り込むスコープが必要ならそれは[[Call]]、[[Construct]]を持たないオブジェクトであるほうが自然です……たとえばMathオブジェクトのようにプロパティにオブジェクトそのものが束縛された状態。
例えば、このようなメタ操作のことを特にjavascriptに限らず魔法と呼ぶのでそのスコープはMagicか、魔法と呼ぶ習慣がない世界を考慮するならMetaObjectを名付けるのが良いでしょう。これで関数とは関係ない(そして特別な)操作が関数というスコープから切り離されMetaObjectというより相応しいスコープに存在することになります。

しかし、コンストラクタに関数群を持たせるほうが良い例外が一つだけあります。それは「汎用化」です。汎用化とはArrayとStringに施された変更の一つでArray.prototype.foo.call()の(ある関数を別のスコープに放り込んで使いやすくするという意味で)分かりやすいショートカットです。

ただし、ただのショートカットに留まらず汎用化には(より厳密な型が求められていた場面で)array kikeをArrayとして扱う。join()を持ったArrayを文字列として扱うなどの利便性向上も含まれた総合的な仕様です。この様に○○というメッセージを持っているなら××として扱うというやり方こそjavascriptのポリモーフィズムであったことを思い出して下さい。

汎用化はcallの明示的呼び出しを伴わない直感的なショートカットとポリモーフィズムに基づいた振る舞いの強化という二つの観点があるためより有用です。

この有用さは関数とは直接関係なくてもより大きな有用さがあります。この様な特別に有用な考慮すべき利点がある場合は関数のプロパティとして追加したほうが受けられる恩恵が大きくなるでしょう。それが追加先の関数とあまりかけ離れていないのであれば考慮できる点です。

逆にただの静的メソッドとして考えるべきではありません。javascriptの(関数オブジェクト含む)オブジェクトは環境に存在する時点で"すでに"作られていてスタンドアロンに存在するからです。静的メソッドは静的な影響を受けます。
"すでに"作られているオブジェクトに対して出来るからといって動的に静的メソッドを追加するといった事はふさわしくありません。逐一こんなことをしていては肥大化とスパゲッティー化とモンキーパッチ問題を引き起こします。

また、静的メソッドとはクラス・ベースの世界においてインスタンスではなくクラスのスコープにメソッドを必要とするという意味です。javascriptではそのスコープをクラスとする必要はありません。なぜならクラスは静的な影響を与える(動的にクラスを変更することによる影響と元々クラスが持つ静的な影響を与える性質を混同しないように……)ものですが、javascriptは動的なインスタンス・ベース(プロトタイプ・ベースとインスタンス・ベースを混同しないように)だからです。インスタンスが動的であるため本来静的な影響は受けません。なので静的メソッドというものはありません。シンタックスがそう見えてもセマンティックスから見ればそれは幻想です。(実際es6にstatic入れるかどうか議論されてました/結局入りました)

すべてのオブジェクトはインスタンスである

っていうのはもう話したっけ?

まあ、いいか──純粋なオブジェクト指向ではすべてのオブジェクトはインスタンスです。ちなみに今回はメモ的な雑多。



es6でも以下のように書いてある。
Any object that is not an ordinary object is an exotic object.


javascriptの場合、プログラムコードからnewできるのはプロトタイプオブジェクトのインスタンスだし、でもプロトタイプオブジェクト自身は空のObjectオブジェクトなんだよね……。

es6だとordinary objectsが追加されたけどordinary objectがプロトタイプオブジェクトの唯一の対になるとは明言されてない──host objects(built-in object)である可能性があるからだろう──で、これは恐らくtriangle operator(こっちは仕様から外された)とclass definition(こっちは構文糖+改良)をやりたかったから。

純粋なオブジェクト指向だと継承関係をユーザー定義するには仕様に明示された(見える)メタオブジェクトが必要になってくる。smalltalkでクラスもオブジェクトだからメタクラスがあるのと同じ理屈。じゃないと(js2.0じゃなく1.5みたいに)全部がルートオブジェクトの直接の子になる。

ここら辺は代入モデルだとわかりやすくモデル化出来ない。

束縛モデルなら──
『たま』とは【猫】である。

と定義できるけど代入モデルだと──
『たま』は【猫】を指す。

となる。そうすると【猫】について聞かれると束縛モデルだと「『猫』とは【○○】である」と【○○】自身を説明できるけど、代入モデルだと「『猫』は【○○】を指す」となって、これは【○○】の実体はどこかにあってそれを参照しているとしか言及していない。
で、その実体について説明すると──

○○という箱(メモリ空間の特定の範囲)に決められたレイアウトに従って敷き詰められた××を指していて、××という箱は……となって最終的に意味のないプリミティブなビットパターンが出てきてやっと終わり。Cの構造体とかC++のthisポインタなんかがまさにこれ。

実体について直接述べているかどうかの違いで束縛モデルの方がオブジェクト指向と相性がいい。
でも束縛モデルは関係なしにオブジェクトをどこまでメタに見るかという問題が先のユーザー定義の継承関係の話に戻る。

束縛モデルなら「ここまでメタに見るよ」って決めたところまで「『○○』は【××】である」と述べ続ければ良い。何ならメモリ空間にまでも言及出来る。

最終的にはdestructual typeを説明するのにnominal typeを定義しなきゃいけないけど──es6の8.2.1 Data Blocksがあるけどこれは単にjs2.0のmachine typesだからnominal typeじゃなくてただのスカラ型──いちおう、仕様にはdestructual typeとnominal typeはない。

es6の仕様ではjs2.0/es4でやろうとしたことをいくつか反映されてるけど、仕様内部の追加で見た目はまだ変わってない。
たぶんこの仕様だけ見ても将来何がしたいか検討付かない人が沢山いるんじゃないかと、(environment recordの記述を改善点と見ても)相変わらずecma-262は仕様書だけ見てもさっぱりわからない仕様だと思う。