性能
ほとんどのベンチマークは、表示に使うピクセルほどの価値もありませんが、人々は好きなようなので、いくつか示します。
メソッド呼び出し
| wren | |
|---|---|
| luajit (-joff) | |
| ruby | |
| lua | |
| python3 | |
| python |
DeltaBlue
| wren | |
|---|---|
| python3 | |
| python |
二分木
| luajit (-joff) | |
|---|---|
| wren | |
| ruby | |
| python | |
| python3 | |
| lua |
再帰的なフィボナッチ計算
| luajit (-joff) | |
|---|---|
| wren | |
| ruby | |
| lua | |
| python | |
| python3 |
棒が短いほど高速です。各ベンチマークは10回実行し、最も速かった時間を採用します。測定するのは、ベンチマーク対象のコード自身を実行する時間だけで、インタープリターの起動時間は含みません。
私のMacBook Pro、2.3 GHz Intel Core i7、16 GBの1,600 MHz DDR3 RAMで実行しました。比較対象はLua 5.2.3、LuaJIT 2.0.2、Python 2.7.5、Python 3.3.4、ruby 2.0.0p247です。JITコンパイルを許可しないプラットフォームもサポートしたいので、LuaJITはJITを無効にして、バイトコードインタープリターモードで実行しています。JITを有効にしたLuaJITは、Wrenも含め、ここで測定したほかのすべての言語よりはるかに高速です。Mike Pallは、未来から来たロボットなのです。
ベンチマークの実行基盤とプログラムは、こちらにあります。
Wrenが高速なのはなぜか? #
言語の性能は、大まかに四つのグループに分かれます。遅いものから速いものへ並べると、次のとおりです。
構文木をたどるインタープリター:Ruby 1.8.7以前、Io、大学の授業で作ったインタープリター。
バイトコードインタープリター:CPython、Ruby 1.9以降、Lua、初期のJavaScript VM。
JITコンパイルする動的型付け言語:現代のJavaScript VM、LuaJIT、PyPy、一部のLisp/Scheme実装。
静的型付け言語:C、C++、Java、C#、Haskellなど。
最初のグループの言語は、ほとんどが本番利用に適していません。(サーバーは例外の一つです。遅い言語へ、より多くのハードウェアを投入できるからです。)二つ目のグループは、挙げた言語の成功が示すように、クライアントのハードウェアでも、多くの用途に十分な速度があります。三つ目のグループは、かなり高速ですが、その実装は驚くほど複雑で、静的型付け言語のコンパイラーに匹敵することもよくあります。
Wrenは、二つ目のグループです。実用に十分な速度を持つ単純な実装が欲しいなら、ここが適したところです。さらに、Wrenには、いくつかの工夫があります。
コンパクトな値の表現 #
動的言語の実装で中心になるのは、変数に使うデータ構造です。どの型の値でも保存、または参照でき、それと同時に、できるだけコンパクトである必要があります。Wrenは、このためにNaNタグ付けという手法を使います。
Wrenでは、すべての値を内部で、小さな8バイトの倍精度浮動小数点数として保存します。これはWrenの数値型でもあるので、算術演算では、「生の」数値へアクセスする前の変換が不要です。数値を保持する値は、有効なdoubleそのものです。これで、算術演算を高速に保ちます。
ほかの型の値を保存するために使える、未使用のビットが、NaNのdoubleには大量にあります。そこへ、ヒープに割り当てたオブジェクトのポインターを格納でき、true、false、nullなどの特別な値に使う余地も残ります。つまり、数値、真偽値、nullはボックス化しません。また、値全体はわずか8バイト、64ビットマシンのネイティブなワードサイズです。CPUキャッシュと、値を受け渡すコストを考えると、小さいほど高速です。
固定したオブジェクト配置 #
ほとんどの動的言語は、オブジェクトを、名前付きプロパティーの緩い集まりとして扱います。作成後も、自由にプロパティーを追加・削除できます。LuaやJavaScriptなどには、オブジェクトの「型」という明確な概念すらありません。
Wrenは、厳密にクラスを基礎とします。すべてのオブジェクトは、クラスのインスタンスです。クラスには、明確な宣言構文があり、命令的には変更できません。さらに、Wrenのフィールドは、クラスに対して非公開であり、そのクラスへ直接定義したメソッドからしかアクセスできません。
これらを合わせると、コンパイル時に、オブジェクトのフィールドが何個あり、それぞれ何かを、正確に決められます。ほかの言語では、作成時に初期メモリーを割り当てますが、フィールドの追加でオブジェクトが大きくなると、何度も再割り当てが必要になる場合があります。Wrenは、最初に一度だけ、正確なフィールド数に合う領域を割り当てます。
同様に、ほかの言語でフィールドへアクセスするとき、インタープリターは、オブジェクト内のハッシュテーブルで名前を探し、見つからなければ、継承の連鎖をたどる場合があります。フィールドは自由に追加できるので、毎回行う必要があります。Wrenでは、コンパイル時に分かるオフセットを使い、インスタンスのスロットへアクセスするだけです。わずかなポインターの加算で済みます。
メソッドをコピーする継承 #
オブジェクトのメソッドを呼び出すには、そのメソッドを見つける必要があります。オブジェクトのクラスへ直接定義したものかもしれませんし、親クラスから継承したものかもしれません。そのため、最悪の場合、見つけるために継承の連鎖をたどる必要があります。
高度な実装は、これを最適化する巧妙な処理を行いますが、言語自身が変更可能であることが難しさを増します。既存クラスへ自由にメソッドを追加したり、継承階層を変更したりできると、あるメソッドの探索結果が時間とともに変わる場合があります。それを確認する必要があり、CPUサイクルを消費します。
Wrenの継承階層は静的であり、クラス定義時に固定します。継承したメソッドは、今後変わらないと分かるので、子クラスを作るとき、すべてをそこへコピーできます。そのため、メソッドのディスパッチは、レシーバーのクラス内でメソッドを探すだけで済みます。
メソッドのシグネチャ #
Wrenは、シグネチャの概念を使い、引数の個数によるオーバーロードをサポートします。これにより、言語の表現力と速度が増します。メソッドを呼び出すとき、レシーバーのクラスで探します。見つかれば、そのメソッドの引数の数も正しいと分かります。
これにより、多くの言語で、メソッドへ渡した引数が少なすぎたり多すぎたりしないか実行時に行う、追加の確認を省けます。Wrenでは、誤った個数の引数でメソッドを呼び出すことは、構文上できません。
計算型goto #
サポートするコンパイラーでは、Wrenのバイトコードインタープリターの主要ループに、計算型gotoというものを使います。バイトコードインタープリターの頻繁に実行する中心部分は、実質的には、実行する命令を対象とした巨大なswitchです。
これを実際のswitchで行うと、CPUの分岐予測器を混乱させます。インタープリター全体に、事実上一つの分岐点しかないからです。予測器はすぐに飽和し、混乱して何も予測できなくなり、CPUの停止やパイプラインのフラッシュが増えます。
計算型gotoを使うと、各命令の末尾に、別々の分岐点ができます。それぞれが独自の分岐予測を使え、ほかより多い命令の組み合わせがあるため、予測が成功することがよくあります。私の大まかなテストでは、性能に5~10%の差が出ます。
単一パスのコンパイラー #
コンパイル時間は、言語の性能の比較的小さな部分です。コードは一度コンパイルすれば済みますが、一行のコードを何度も実行する場合があります。しかし、コンパイルが速いと、何かを起動して動かすまでの起動時間に役立ちます。その点で、Wrenのコンパイラーはかなり高速です。
Luaのコンパイラーを手本にしています。トークン化してから解析し、後の段階で消費・解放する大量のAST構造を作る代わりに、解析中に直接コードを出力します。そのため、解析中のメモリー割り当ては最小限で、余分な負担がほとんどありません。
ほかの言語が同じことをしないのはなぜか? #
Wrenの性能の多くは、言語設計の判断から生まれます。型付けとディスパッチは動的ですが、クラスの定義は比較的静的です。そのため、多くのことが容易になります。ほかの言語は、オブジェクトのモデルがはるかに変更可能であり、大量の既存コードを壊さずに変えることはできません。
Wrenに最も近い仲間は、圧倒的にLuaです。LuaはWrenより動的であり、その分、実装が難しくなります。また、Luaは、幅広いハードウェアとコンパイラーとの互換性を保つため、非常に努力しています。C89コンパイラーがあれば、そこでLuaを動かせる可能性は、とても高いでしょう。
Wrenも互換性を重視しますが、C99またはC++98と、IEEEの倍精度浮動小数点数を必要とします。そのため、一部の特殊なハードウェアを除外するかもしれませんが、NaNタグ付け、計算型goto、ほかのいくつかの工夫を使えます。