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

FirefoxOperaは2^31に境界があり、ChromeSafariは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文についてのパフォーマンスの違いを見ていく予定です。