動的言語内で扱う値の内部表現に関する最適化で、NaN Boxingという方法を知ったのでメモ。
あらまし
動的言語を作るという内容の本でオンラインでも読めてすごくおすすめなCrafting Interpretersの最後の章の最適化の話の中でNaN Boxingについて書かれている。
いわゆる動的言語では変数が型情報を持っているんじゃなくて値自体で型を区別できるようになっている。
普通に実装するには内部表現として型情報とその型に関するデータとして持つことになるが、NaN Boxingではそれをまとめた形で持つようにする。
CPUで扱える1ワード(64ビットint
)で表現することで持ち運びやコピーが簡単になり、キャッシュも効いて高速化するという利点がある。
普通のやり方
typedef enum { |
Value
構造体に型の種類を判別するためのValueType
と、型ごとの情報を共用体as
で持つ(Tagged unionと呼ばれる)。
double
やObj
ポインタのアライメントがあるので、Value
のサイズは16バイトになる。
これをどうにかしてもっと短縮したい、というエクストリームプレイ。
NaN Boxing
大まかにはdouble
とポインタどちらも64ビット長だけど、どうにかして一つの64ビットで型も判別できるようにした上でさらに値も保持する、というテクニック。
浮動小数点数のフォーマットはIEEE754で決まっていて、符号1ビット|指数部11ビット|仮数部52ビットの計64ビットで構成されている。
普通の数値では指数部は1〜2046(ゲタ1022)だが、NaN
は特別で指数部11ビットがすべて1となっている。
ただNaN
は無限大と非数にしか使われず仮数部最上位の1ビット以外は使用されないので、それを使う。
64ビット環境のポインタも今の所実際には下位48ビットしか使われてないので、うまく詰め込める(※)。
なので判定手順は
- もし指数部+仮数部上位2ビットが1(
QNAN
)でないなら、数値(浮動小数点数) - 全て1なら:下位の値によって
- 1:
nil
- 2:
true
- 3:
false
- 他:オブジェクトへのポインタ(この場合のみ最上位の符号ビットが立ってる)
- 1:
実際にテスト
テストコード:ideone.com
抜粋:
typedef uint64_t Value; |
$ cc nan-boxing.c && ./a.out |
その他
- 約10%高速化、との話
- 環境によってはNaN Boxingが使えない場合に備えて、
#ifdef
で分岐できるようにしている double
との型変換に無理やりキャストじゃなくて、memcpy
使うんだぁ…(最適化で消えるらしい)- cloxでは整数型はないけど、
nil
や真偽値のタグをもっと上位に移動させて、整数タグの場合には下位32ビットを使用すれば追加できそう - 現行の64ビットアーキテクチャでもハードウェア的にやカーネル的に全アドレスが使用されるわけではなく、アドレスバス(52ビット?)やページテーブル(48ビット?)自体がフルに使うようにはなってないらしい(要出典)
リンク
- Optimization · Crafting Interpreters
- Tagged pointer 浮動小数点数を使わない場合、タグを下位に埋め込んだりしたもんだった