実装は大変だ

TypedObjectsってなんだ!

TypedObjectsは何に使うんだと思った人はいませんか?
あれはこう使います──

//type operator
type T = { x: { a: int, b: double }, y: string };
//special type operator(like operator)
function g(): like T {
return { x: { a: 3, b: 7.0 }, y: "foo" };
}
let { x: { a, b }, y: c }: T = g();

これでxに{ a: int, b: double }型のオブジェクト、yにstring型の値が束縛されます。
type operatorは値制限を行うもの。変数名のあとの:fooはannotationsといって型注釈です。
javascriptの型システムはstructuralですが上のコードを見ると型注釈が付いているのでそのプロパティの型が型注釈で指定したnominal typeと互換でなければいけません。関数gの戻り値を見ると注釈はlike Tです。likeはlike operatorといって型変数の一致する変域が、構造が一致するか、またはプロパティのnominal typeまで一致するかのどちらかでなければいけません。

つまりgの戻り値を受け取る変数の型が{ x: { a, b }, y }かtype Tでなければならないということです。

そして、type Tの定義を見てみると右辺が{ x: { a: int, b: double }, y: string }となっています。これはrecord typeといって新たなオブジェクトの型を定義するときに使います。

さて、ここまではjs2.0/es4の話です。本来の使い方です。また、es4では型とクラスの生成はrun-timeではなくcompile-timeに解決されます。そうです、early-bindingしてます。

しかし、es4と違ってes6には(建前上は)early-bindingがなく、型注釈も値制限もありません。そこで、record typeがes6でtyped objectに変化します。

ここで一度esのwikiを見てみましょう。
http://wiki.ecmascript.org/doku.php?id=harmony:typed_objects

Some use cases:

optimized data abstractions - these will be very easy to optimize well in JIT’s
binary serialization
optimized data representations for compilers that generate JavaScript
communicating structured data (such as arrays of records) to WebGL

と書かれています。「とても簡単にコンパイル時の最適化とシリアライズとコンパイラ表現の生成とネイティブライブラリとの通信に構造化されたデータが使えます」といった感じですが、lite-bindingされてrun timeに最適化するよりearly-bindingしてcompile-timeした方がもっと簡単に、そして効率良く最適化出来ます。そして、ライブラリとの通信、es4にはparameterized typeと言って型付された配列とマップがあるので本来はそれでやります。

では結局、何がやりたかったんだというと本来の仕様と違うので本来の使用目的には使えないが仕様を変えて無理やり追加して妥協しましたというだけです。つまり、ここに挙げられた用途に本当に相応しいかというのは熟考の余地があります。

このようにtyped objectはes4から自然選択ではなく逆選択的にes5/6に取り入れられた仕様のごく一部です。

なんでこんな話かというと、実装者向けの話になります。

es4がさんざん採用されなかった言い分には仕様変更がデカすぎるということがずっと言われ続けてきましたが、しかし、es5/6ではes4が文法機能として取り入れようとしたものをライブラリ側でAPIとして実装する傾向にあります。さらにes4ではself-hosttingといって仕様で定義されているオブジェクトはes4で書かれているという仕様があります。これで使用の穴をついた非互換が防げるだけでなくself-hosttingされている部分は処理系毎に用意する必要がないので生産性が上がります。そのため、実装者はAPIの実装ではなく言語仕様の実装とテストに時間が割けます。

昔、モジュールを入れてライブラリを強化しようとしたらライブラリがデカいと実装が大変だという理由で長い間、拒否され続けていました。しかし現在ではモジュールが導入され名前空間には大量のAPIが存在します。そこにはes4の言語機能をAPIで実装したライブラリ以外にもes4に全く無かった新しいライブラリも含まれます。

これが意味するところは簡単で結局es4より、もとのモジュールより、現在のes6の方が実装量が増えています。たとえ苦労してes6を実装して全テストを通したとしてもそれはes4の成り損ないでしかなく、新しい言語仕様でスマートに簡潔に出来た高度な機能をes3ベースでは実現できないし、頑なに言語仕様を変えたくないという強い意志があるのでライブラリとして無理やり実装してスマートでもなく、冗長で、低機能なあたかもそれがes6の新機能であるかのように振る舞う劣化es4体験をユーザーに提供します。

そしてそのコストはすべて実装者が肩代わりします。良いところが変わらず、ありのままで居続けた仕様といえばrest argumentsくらいでしょう。それでもrest parameterと名前が変わってしまいましたがこれはArgumentsオブジェクトを置き換える良い新機能です。

しかし、幾多の苦労を乗り越えて自然選択されたものがたったこれだけでは今までの歳月と今からes6を実装する歳月とを足すととても割に合いません。しかも順調ならとっくに実装されていたであろうものが10数年の大遅刻を経てやっとたった一つやって来たにすぎません。

……と、rhinoを全面的に変更していてes6を実装するかどうか考えたものです。

下位互換だけが大義名分ではありません。

webの混乱よりwebの停滞のほうが問題です。

──とes6を実装する意義には疑問を拭えません。

長くメンテされてきたソースコードはいずれ捨てなければならないのとスパゲッティーを改修するには時間がかかるという事を今までの議論で全く考慮されなかったのでしょう。

nashornとrhinoの違い

oracleの言うnashornの速いとは何か?

結論から言ってそれはスループットだ。nashornはサーバー用途なので当然だろう。
サーバーでは豊富なコンピューター資源をありったけ消費してできる限りのタスクをこなさねばならない。

では、rhinoはスループット重視の処理系かと言うとそうではない。rhinoはフットプリントとレスポンス重視の処理系だ。なのでV8 benchmark suteの結果は悪いがsun-spiderの結果は良い。しかし、まあウォームアップさえ行えばnashornは今のところrhinoより100~300msほど速くなる。

だがnashornはバイトコード生成中にrhinoの約2倍のCPU負荷、それと常に約100Mbyteほど多いメモリを消費する。たかだか平均150msの違いにrhinoのCPU負荷が25%のときに50%、メモリ使用量が400Mbyteのとき500Mbyte消費し、ウォームアップ時間まで要求することを意味している。

バイトコードが生成されたスクリプトが2回以上実行されることが条件だが、それ以降のCPU負荷はrhinoと同等まで落とすことが出来る。しかし、最大300msほどのレスポンスのアドバンテージを得るのに必要なウォームアップは2回や3回ではすまない、その程度だとせいぜい100msくらいだ。これくらいならrhinoの最適化オプションをいじればいい。しかし、メモリ使用量はどうにもならない。常に100Mbyteほどnashornの方が多い。
ちなみにコマンドから起動すると-Xms8mオプションが指定されていて少なすぎるので多めにするといい


これらの事からこの2つの処理系の大きな違いが分かるだろう。

一番初めにいったようにnashornは豊富な資源を活用しウォームアップをしてjavaのJITコンパイラに最適化させ、より多くのタスクをこなす。そしてrhinoは資源も気にせずただ素早くレスポンスを返す。

どちらにせよ、nashornもrhinoも最近のブラウザの実装とはスループットもフットプリントも比べ物にならないことに違いはないが、重要なのは2つの実装には明確な違いがあるということ。

さて、rhinoにはもう一つの特徴があることを伝えていたはずだ。そう、フットプリントだ。rhinoのフットプリントに影響するビルドオプションは充実していて全部入りで1.1Mbyteくらいから最小で800Kbyteくらいまで小さくすることが出来る。(nashornのフットプリント? 角のある赤い指揮官機くらい違うよ!)

とまあ、ここでnashornはOpenJDKに採用されたんだからフットプリントは関係ないんじゃないかと思う人もいるだろう。

だが、InternalパッケージはJREからは使ってはいけないということを忘れてはいけない。

Internalパッケージは実装毎に違うからね。じゃあ、scripting apiを使えばいいじゃないと?

ただスクリプトを呼び出すだけならそれでいい。なぜならscripting apiはそれだけしか出来ないからそれが唯一仕事だ。

もし処理系に新たなオブジェクトを追加したくなったらどうする?
もしセキュリティについて考慮しなければいけないときどうする?
バグフィックスは?
セキュリティフィックスは?

scripting apiからはそれが出来ない、術がない。どちらの実装も-Dオプションでセキュリティの挙動を変えられるがそんなもの実装依存だ。javascriptからjavaを呼び出せる? それはjavaのオブジェクトだ。javascriptのオブジェクトではない。フィックスに関してもnashornの変更がopenJDKへ反映されるのはjdkのupdateのタイミングだ。オリジナルのコミュニティからはラグがある。

だから結局、アプリケーション拡張のための組み込み言語などの用途ではソースコードからビルドしてアプリケーションにバンドルする必要がある。scripting apiに何かを求めてはいけない。あれは間違いなく失敗作だ。

Rhinoの最新のmasterブランチとinvokedynamicブランチのmerge

fast-forward成功したどーーーーー!

バグで動かないのでバグ取りが必要なのと、実は遅くなっているという問題があるんですがね。

まあ、ここまでは予測していました。invokedynamicブランチは1.7R3の安定前のコミットで1.7R4より元々遅いという事とjava側が深すぎるという事、それともう一つ、現状型変換をランタイム側でやっているのでMethodHandleの恩恵を受けられないであろうと考えていました。MethodHandleのシグネチャはポリモーフィックかつboxing/unboxing変換は向こうが勝手にやってくれるのでスクリプト側は実際に必要な型で扱えばいいのです、むしろスクリプト側で変換用グルーコードを差し込んではいけません。最適化をVMに丸投げ出来なくなってindyの効果を発揮できません。LiveConnectのようなスクリプト言語独自の型変換だけをやって他はjavaのinvokedynamicライブラリとVMに丸投げしなくてはいけません。でなきゃ悲しいことにインライン化とかやってくれません。(正確には丸投げすればインライン化出来るはずであったチャンスを失う)

V8が出てくるまではjsの実装の中でも速い部類だったし、むしろ手書きで速いってのは使ってる人間には昔から知られてたことだったけど、手書きでここまでせんでも……ソースコード読んでもバイトコード読んでもなんでコレで速くなるのか、魔法の意味が理解できぬ!

たぶん試行錯誤してJITコンパイルされた時に速くなるようにチューニングしたんだろうなぁ。まあ、それ用の変数を見つけたからちょっといじったらv8-benchmarks-v6のスコアが1割弱上がったんだけどメモリ使用量の面でかなわない──結局、現状が最適だという結論に落ちる。魔法恐るべし。

コード書く前に魔法のお勉強しなきゃいけないからnashorn projectがrhinoのコミッター返してくれないかな?

rhino雑記

今、作業中のrhinoの変更はと言うとIdFunctionCallインターフェイスを用いずに直接Prototype objectやinstanceのpropertiesを呼び出せるようにしている最中です。
IdFunctionCall#execIdCallではindyで呼び出すにはシグネチャの指定が不十分なのとjavaの呼び出し階層が深すぎるため効率が悪いのと、現状の実装ではexecIdCallの中にすべてのpropertiesの振る舞いを実装しているのでJITコンパイラが最適化するとき効率が悪いためそれらに対処する必要があります。

「javaの呼び出し階層が深すぎる」というのはrhinoでは最近のインスタンスベースやプロトタイプベースの実装とは違って内部でメソッド検索をするための構造がクラスのような単純に位置を特定できる構造にはなっていないのでコールスタックが自然と深くなってしまいます、それとJavaは関数を第一級として扱えないためメソッドを持ったクラスを関数インターフェイスでラップするといった実装になっています。ここで更にコールスタックが深くなるためindyを用いたときに効果を発揮できません。これらの事情があるためここをどうにかしないといけません。

他にもやらなければいけないことが山ほどあって、一つに古すぎるコードの刷新が必要になります。rhinoでは初期実装からjs側ライブラリの実装コードがほとんど変わっておらず、その発祥は元を辿ればBrendanが昔に書いたspidermonkeyのベタ移植が元祖です。パフォーマンステストをしてみると、どうもこの部分が遅いため足かせになっているようです。正規表現が一番遅いようでrhino付属のv8-benchmark-v6にてVisual VMを用いてみるとセルフタイムの3割が正規表現に費やされています。次にスロット検索周りとShellのGlobalオブジェクトに在るprint関数でした。ここらも高速化が必要です。

ただ、単純に高速化といってもrhinoは経験則と手書きでガチガチに最適化されているので小手先の修正が効きません、というか魔法過ぎて本番環境でテストしてみないとパフォーマンスが測り得ません。たとえばRhinoでは式を評価するたびにDoubleラッパーをnewしているので一見遅そうに見えます。そこでmicro-benchmarkレベルで2倍速いコードに書き換えてみてもよい影響がありませんでした。むしろ、キャッシュの分、使用メモリが増えていつGCが起こるか予測がより困難になっただけでした。

「式を評価するたびにDoubleラッパーをnew」というのは全体に影響していてRhinoは数値計算が苦手です。例えば

for(let i=0; i<100000; i++){

}

rhinoではこの様な単純計算が一見遅いかの様に見えます。しかし、先に挙げたとおりこの様なコードの特有の遅さは本番では他の部分で十分に吸収されているようでこのコードが遅いという事実に全く意味を成しません。jsコードだけで見ると先の正規表現のように一部の実装が極端に遅いことが原因で、LiveConnectを使ってjavaを呼び出してみると制御を移した先のjava部分が遅いことが原因であったりします。アプリケーション拡張のような使い方だとスクリプトはただのグルーコードなのでこの事が問題になるかと思います。

たとえば呼び出し先がJOGLだとしたらJOGLからJNIを呼び出している部分が遅いようです。これはJNIコードがそれ以上最適化出来ないので当然のことですがRhino側からはどうしようもありません。

そこで根本的にrhinoを早くしてしまわなければならないのですがそれがそうも簡単にいかないというお話でした。

Rhinoに動きが・・・

Rhinoに動きが・・・

ちょっと待って!フォークして大改変してるところなのに今再開されるとマージしきれない!

話を要約するとRhinoのコミッターがnashornにごっそり持っていかれたけどまだrhino必要だから開発続けて欲しいと、それでフォークするかどうしようかって話してるところみたい。

Hannesまで出てきてフォークしそうだなぁ。こりゃgoogle groupに顔出さないとついて行けないぞ。