JavaScriptのパフォーマンスの違いから数値表現の違いを推定してみた
はじめに
JavaScriptの数値は仕様的には64bit倍精度のIEEE754に準拠というものですが、内部の実装では高速化のために必要に応じて32bit整数だったりdoubleだったりします。ブラウザによっては、その辺りの実装に違いがある筈で、それはパフォーマンスの違いとなって現れるのではないかと予想したので、それを測定してみました。
ベースラインの測定
そのためには、ベースラインをはじめに測定する必要があります。C言語でint32型、int64型、double型のパフォーマンスを測定します。C言語レベルでint型とdouble型のパフォーマンスの違いが観測されなければ、JavaScriptの内部の実装でそれらを区別して実装していても、外部からはその違いが観測されないでしょう。
下記の簡単な関数を実行したときの時間を計測しました。
void perf_int32(int n) { volatile int32_t v = 123; while (n-- > 0) { v++; v *= 3; v /= 3; v--; } } void perf_double(int n) { volatile double v = 9876543210.123; while (n-- > 0) { v++; v *= 3.0; v /= 3.0; v--; } } void perf_int64(int n) { volatile int64_t v = 9876543210LL; while (n-- > 0) { v++; v *= 3LL; v /= 3LL; v--; } }
シフト演算をするような最適化がされないように、掛け算と割り算にあえて3を使っています。また、whileループで変数vの値は変わらないのでそこも最適化されないようにvolatile宣言しています。アセンブルコードでも確認しましたが、最適化はないようです。
結果は以下のようになりました。
n | 5,000,000 | 10,000,000 | 50,000,000 | 100,000,000 |
int32 | 138 | 272 | 1332 | 2772 |
double | 308 | 623 | 3131 | 6570 |
int64 | 442 | 889 | 4470 | 8932 |
doubleはint32型より約2.3倍、int64はint32より約3.2倍時間かかることがわかります。int64はdoubleより遅いので、int32を超えるときの整数演算として使う理由はないと思われます。
JavaScriptでのint32とdoubleのパフォーマンスの違いの確認
ベースラインで違いがあることが確認できたので、もし、JavaScriptエンジンの実装でNumber型としてint32とdoubleを区別して使っていたら、JavaScriptプログラムの実行でも処理時間の差となって観測されることが予想されます。
測定した各ブラウザのバージョンは以下の通りです。最新のバージョンではありませんが、速度に大きな違いはないと思われます。
Firefox | Chrome | Safari | Opera |
3.5.5 | 4.0.223.16 | 4 Public Beta(528.16) | 10.00 |
まず、32bitより十分に小さい数値と32bitより十分に大きい数値での違いを測定してみました。
function perf_test(val) { var cnt = 10000000; var tm1 = (new Date).getTime(); while (cnt-- > 0) { val++; val--; } var tm2 = (new Date).getTime(); alert("time=" + (tm2 - tm1)); }
perf_test関数の引数valに123と9876543210を与えたときの結果は以下の通りです。
val | Firefox | Chrome | Safari | Opera |
123 | 119 | 121 | 170 | 2171 |
9876543210 | 191 | 1947 | 1974 | 4125 |
ここで、9876543210に対して++や--は有効な結果となります。つまり、桁落ちや丸めは発生しません。
これを見ると、確かにパフォーマンスの違いとなって現れました。123はC言語レベルではint32型の計算として実装されていると予想されます。一方、9876543210の方はint32型では表現できません。
もし、9876543210がdoubleで計算されているとすれば、valが浮動小数の9876543210.123のときとパフォーマンスは変わらない筈です。
確認してみましょう。
val | Firefox | Chrome | Safari | Opera |
123.456 | 205 | 1970 | 1833 | 4234 |
9876543210.123 | 205 | 1956 | 1807 | 3875 |
結果は、多少誤差はあるものの、予想通りになりました。つまり、各ブラウザのスクリプトエンジンはNumber型に対して、小さい整数のときはint32型を、大きい整数や浮動小数のときはdouble型として区別していることが想像されます。
int型とdouble型の境界値の測定
では、int32とdoubleの境界はどこにあるでしょうか。2^31, 2^30, 2^29 前後の数を調べてみます。
2^31 = 2147483648 2^30 = 1073741824 2^29 = 536870912
val | Firefox | Chrome | Safari | Opera |
2147483649 | 205 | 1980 | 1832 | 3859 |
2147483647 | 120 | 1944 | 1872 | 2141 |
1073741825 | 120 | 1929 | 1890 | 2125 |
1073741823 | 120 | 120 | 136 | 2125 |
536870913 | 120 | 120 | 127 | 2125 |
536870911 | 120 | 120 | 120 | 2125 |
FirefoxとOperaは2^31に境界があり、ChromeとSafariは2^30 にintとdoubleの境界があるようです。int32の最上位ビットは符号なので、2^31に境界があるのは分りますが、2^30に境界がある理由はソースを見ないとわかりません。
ついでに、NaNとInfinityも測定してみました。
val | Firefox | Chrome | Safari | Opera |
NaN | 205 | 5372 | 9431 | 6406 |
Infinity | 205 | 5337 | 6441 | 6297 |
Firefoxは通常のdoubleと同じですが、それ以外はdoubleより多くの時間がかかっています。C言語レベルでIEEE754をサポートしていれば、NaNとInfinityは通常のdoubleとして計算して問題ないと思われますが、Firefox以外はNaNとInfinityは特別な数値として陽に扱われているのでしょうか?
double型からint型への変換
次に、double型からint型に変換可能な結果になった場合に、int型になるかどうかを調べてみました。
val | Firefox | Chrome | Safari | Opera |
3.0 | 120 | 120 | 130 | 2140 |
3.5 - 0.5 | 120 | 2113 | 130 | 2265 |
上段の3.0はperf_test(3.0)と直接3.0を指定していますが、下段の3.5-0.5は
val = 3.5; val -= 0.5; perf_test(val);
というコードです。3.5-0.5と直接書くと、さすがにコンパイラが3にするようです。
この結果をみると、Chromeはdouble型の演算結果がint型に変換可能な場合でもdoubleのままのようです。このケース以外にも、3.5*10 相当の場合も同様でした。
まとめ
JavaScriptのNumber型の数値は仕様的には64bit倍精度の浮動小数ですが、ブラウザごとに多少の違いはあるものの、内部の実装では十分小さな整数に対してはint型の演算が行われていることがパフォーマンス測定の結果により推論できます。ソースコードを読めば済むことですが、ソースがなくてもある程度のことが分るという例です。
今回測定したのは、数回の計測を平均したものです。より正確に推論するには、統計的手法を使って有意差の確率を考慮する必要があるでしょう。
次回はswitch文についてのパフォーマンスの違いを見ていく予定です。