V 8は「定数折りたたみ」の最適化テクニックを使用しており、べき乗(**)演算がMath.pow()に等しくない場合があります.
3176 ワード
現在の主流のWebプログラミング言語では、PHPやPythonなど、べき乗演算子(一般的には記号は^または**)が含まれています.最新のES 7ではべき乗演算のサポートも追加されており、記号**を使用すると、最新のChromeではべき乗演算のサポートが提供されています.
しかしjavascriptでは**演算がMath.pow(a,b)に等しくない場合があり、最新のChrome 55では:
Math.pow(99,99)の結果は3.697296376497263 e+1997であり、
しかし99*99の結果は3.697296376497268 e+197であった.
両者は等しくない
3.697296376497263e+197 3.697296376497268e+197
またMath.pow(99,99)-99**99の結果も0ではなく-5.313779928167671 e+182であった.
したがって、**オペレータはべき乗演算のもう一つの実装にすぎないと推測します.しかし、関数を書くと、べき乗演算は奇妙な特性を示します.
呼び出しdiff(99)は0を返す.WTF?両者はまた等しい!
次のコードは何を出力しますか?
このコードの実行結果は、-5.313779928167671 e+182です.
これはまるで薛定谔のべき乗だ.
その原因を究明すると、V 8エンジンは定数折り畳み(const folding)を使用している.定数折り畳みはコンパイラのコンパイル最適化技術である.
次のコードを考慮します.
このループの条件i<100*100*100は式(expression)であり、判断時に値を求めると100*100*100の計算が100000回行われる.コンパイラが構文解析フェーズで定数のマージを行うと、このループは次のようになります.
上記の99**99の計算にも定数折り畳みが用いられている.つまり99*99はコンパイル時に計算(定数折りたたみ)され、Math.powは常に実行時に計算されます.変数を使用してべき乗演算を行う場合(例a**b)は定数折り畳みが存在しないため、a**bの値は実行時に計算され、**はMath.pow呼び出しにコンパイルされます.
ソースコードsrc/parsing/parser.ccファイルで、コンパイル時にコードを計算します.
Pow関数を使用してべき乗演算を計算した評価結果が表示されます.Powはinlineの関数で、内部にはいくつかの従来の最適化が行われており、最適化できない場合はstd::pow(x,y)を使用して最終結果を計算しています.
Math.powのアルゴリズムは次のとおりです.
両者は異なるアルゴリズムを用いていることがわかる.ただし、定数の折りたたみを行わない場合、**はMath.pow関数呼び出しに変換されます.
そこで**がMath.powに等しくない場合があるという奇妙な問題を引き起こした.次のコードを見てみましょう.
個別出力:
3.697296376497268e+197 3.697296376497263e+197 3.697296376497263e+197
実は
9999=369729637649726772657187905628805440595668764281741102430259972423552570455277523421410650010128232727940978889548326540119429996769494359451621570193644014418071060667659301384999779999159200499899
したがって、最初の結果はより正確な値に近い.
先週(2017年1月16日)、この奇妙な行為はすでにバグとしてV 8プロジェクトに提出され、バグ番号#5848.
しかしjavascriptでは**演算がMath.pow(a,b)に等しくない場合があり、最新のChrome 55では:
Math.pow(99,99)の結果は3.697296376497263 e+1997であり、
しかし99*99の結果は3.697296376497268 e+197であった.
両者は等しくない
3.697296376497263e+197 3.697296376497268e+197
またMath.pow(99,99)-99**99の結果も0ではなく-5.313779928167671 e+182であった.
したがって、**オペレータはべき乗演算のもう一つの実装にすぎないと推測します.しかし、関数を書くと、べき乗演算は奇妙な特性を示します.
function diff(x) {
return Math.pow(x,x) - x**x;
}
呼び出しdiff(99)は0を返す.WTF?両者はまた等しい!
次のコードは何を出力しますか?
var x = 99;
x**x - 99**99;
このコードの実行結果は、-5.313779928167671 e+182です.
これはまるで薛定谔のべき乗だ.
その原因を究明すると、V 8エンジンは定数折り畳み(const folding)を使用している.定数折り畳みはコンパイラのコンパイル最適化技術である.
次のコードを考慮します.
for (let i = 0; i < 100*100*100; i++){
//
}
このループの条件i<100*100*100は式(expression)であり、判断時に値を求めると100*100*100の計算が100000回行われる.コンパイラが構文解析フェーズで定数のマージを行うと、このループは次のようになります.
for (let i = 0; i < 1000000; i++){
//
}
上記の99**99の計算にも定数折り畳みが用いられている.つまり99*99はコンパイル時に計算(定数折りたたみ)され、Math.powは常に実行時に計算されます.変数を使用してべき乗演算を行う場合(例a**b)は定数折り畳みが存在しないため、a**bの値は実行時に計算され、**はMath.pow呼び出しにコンパイルされます.
ソースコードsrc/parsing/parser.ccファイルで、コンパイル時にコードを計算します.
case Token::EXP: {
double value = Pow(x_val, y_val);
int int_value = static_cast(value);
*x = factory()->NewNumberLiteral(
int_value == value && value != -0.0 ? int_value : value, pos,
has_dot);
return true;
Pow関数を使用してべき乗演算を計算した評価結果が表示されます.Powはinlineの関数で、内部にはいくつかの従来の最適化が行われており、最適化できない場合はstd::pow(x,y)を使用して最終結果を計算しています.
Math.powのアルゴリズムは次のとおりです.
// ES6 section 20.2.2.26 Math.pow ( x, y )
TF_BUILTIN(MathPow, CodeStubAssembler) {
Node* x = Parameter(1);
Node* y = Parameter(2);
Node* context = Parameter(5);
Node* x_value = TruncateTaggedToFloat64(context, x);
Node* y_value = TruncateTaggedToFloat64(context, y);
Node* value = Float64Pow(x_value, y_value);
Node* result = ChangeFloat64ToTagged(value);
Return(result);
}
両者は異なるアルゴリズムを用いていることがわかる.ただし、定数の折りたたみを行わない場合、**はMath.pow関数呼び出しに変換されます.
Expression* Parser::RewriteExponentiation(
Expression* left,
Expression* right,
int pos) {
ZoneList* args = new (zone()) ZoneList(2, zone());
args->Add(left, zone());
args->Add(right, zone());
return factory()->NewCallRuntime(Context::MATH_POW_INDEX, args, pos);
}
そこで**がMath.powに等しくない場合があるという奇妙な問題を引き起こした.次のコードを見てみましょう.
console.log(99**99);
a = 99, b = 99;
console.log(a**b);
console.log(Math.pow(99, 99));
個別出力:
3.697296376497268e+197 3.697296376497263e+197 3.697296376497263e+197
実は
9999=369729637649726772657187905628805440595668764281741102430259972423552570455277523421410650010128232727940978889548326540119429996769494359451621570193644014418071060667659301384999779999159200499899
したがって、最初の結果はより正確な値に近い.
先週(2017年1月16日)、この奇妙な行為はすでにバグとしてV 8プロジェクトに提出され、バグ番号#5848.