web-dev-qa-db-ja.com

V8でこのコード・スニペットを使用するよりも<=が遅いのはなぜですか?

私はスライドを読んでいます V8でJavascriptの制限速度を破る 、そして以下のような例があります。この場合、なぜ<=<より遅いのか理解できませんが、誰もが説明できますか?任意のコメントは大歓迎です。

スロー:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

(ヒント:primesは長さprime_countの配列です)

もっと早く:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i < this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

[詳細]速度の改善は重要です。私の地域の環境テストでは、結果は次のとおりです。

V8 version 7.3.0 (candidate) 

スロー:

 time d8 prime.js
 287107
 12.71 user 
 0.05 system 
 0:12.84 elapsed 

もっと早く:

time d8 prime.js
287107
1.82 user 
0.01 system 
0:01.84 elapsed
162
Leonardo Physh

私はGoogleでV8に取り組んでいますが、既存の回答とコメントに加えてさらに洞察を提供したいと思いました。

参考のために、 スライド からの完全なコード例を示します。

var iterations = 25000;

function Primes() {
  this.prime_count = 0;
  this.primes = new Array(iterations);
  this.getPrimeCount = function() { return this.prime_count; }
  this.getPrime = function(i) { return this.primes[i]; }
  this.addPrime = function(i) {
    this.primes[this.prime_count++] = i;
  }
  this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
      if ((candidate % this.primes[i]) == 0) return true;
    }
    return false;
  }
};

function main() {
  var p = new Primes();
  var c = 1;
  while (p.getPrimeCount() < iterations) {
    if (!p.isPrimeDivisible(c)) {
      p.addPrime(c);
    }
    c++;
  }
  console.log(p.getPrime(p.getPrimeCount() - 1));
}

main();

まず第一に、パフォーマンスの違いは<および<=演算子とは直接関係ありません。ですから、スタックオーバーフローを読むのが遅いので、コード内の<=を避けるためだけにフープを飛び越えないでください。そうではありません。


第二に、人々は配列が「ホーリー」であることを指摘しました。これはOPの投稿のコードスニペットからは明らかではありませんでしたが、this.primesを初期化するコードを見ると明らかです。

this.primes = new Array(iterations);

たとえ配列が完全に埋められ/詰まった/連続していても、V8では HOLEY要素の種類 を持つ配列になります。一般に、穴あき配列の操作はパックド配列の操作よりも遅くなりますが、この場合の違いはごくわずかです。1回の追加のSmi(小整数)チェック(穴に対する保護) isPrimeDivisible内のループでthis.primes[i]を打つたびに)大きな問題ではない!

TL; DRHOLEYである配列はここでは問題ありません。


他の人たちは、コードが範囲外を読むことを指摘しました。一般的に、 配列の長さ を超えた読み取りを避けることをお勧めします。この場合、パフォーマンスの大幅な低下を避けることができます。しかし、なぜだ? V8は、パフォーマンスにわずかな影響を与えるだけで、これらの範囲外のシナリオのいくつかを処理することができます

範囲外の読み取りの結果、この行のthis.primes[i]undefinedになります。

if ((candidate % this.primes[i]) == 0) return true;

そしてそれが本当の問題に至ります:%演算子は現在整数でないオペランドで使われています!

  • integer % someOtherIntegerは非常に効率的に計算できます。この場合、JavaScriptエンジンは高度に最適化されたマシンコードを生成できます。

  • 一方、undefinedはdoubleとして表されるため、integer % undefinedは効率的でないFloat64Modになります。

この行で<=<に変更することで、コードスニペットを実際に改善することができます。

for (var i = 1; i <= this.prime_count; ++i) {

<=<よりも優れた演算子であるという理由ではありませんが、これはこの特定のケースでは範囲外の読み取りが回避されるためです。

131
Mathias Bynens

他の回答とコメントは、2つのループの違いは、最初のループが2番目のループよりも1回多い反復を実行するということです。これは事実ですが、25,000要素まで増加する配列では、多かれ少なかれ1回の繰り返しでもわずかな違いしかありません。球場が推測するように、それが伸びるときの平均の長さが12,500であると仮定すると、私たちが予想する違いはおよそ1/12,500、またはたった0.008%であるべきです。

ここでのパフォーマンスの違いは、その1回の追加の反復によって説明されるものよりもはるかに大きく、問題はプレゼンテーションの終わり近くで説明されています。

this.primesは連続した配列(すべての要素は値を保持します)で、要素はすべて数字です。

JavaScriptエンジンは、そのような配列を、実際には実際の数字の単純な配列になるように最適化することがあります。実際には数字を含むが他の値を含む、または値を含まないオブジェクトの配列です。最初の形式はアクセスがはるかに速いです。コードが少なくて済み、配列がはるかに小さいためキャッシュに適しています。しかし、この最適化フォーマットの使用を妨げる可能性があるいくつかの条件があります。

1つの条件は、配列要素のいくつかが欠けている場合です。例えば:

let array = [];
a[0] = 10;
a[2] = 20;

a[1]の値は何ですか?に値はありません。 (値がundefinedであると言っても正しくありません - undefined値を含む配列要素は、完全に欠けている配列要素とは異なります。)

これを数字だけで表現する方法はないので、JavaScriptエンジンはあまり最適化されていないフォーマットを使用することを余儀なくされています。 a[1]に他の2つの要素のような数値が含まれている場合、配列は潜在的に数値のみの配列に最適化される可能性があります。

プレゼンテーションで説明したように、配列が最適化されていないフォーマットに強制されるもう1つの理由は、配列の範囲外の要素にアクセスしようとした場合です。

<=を使った最初のループは、配列の終わりを越えて要素を読み込もうとします。最後の追加の反復では、アルゴリズムは依然として正しく機能します。

  • undefinedは配列の末尾を超えているため、this.primes[i]iと評価されます。
  • candidate % undefinedcandidateのどの値の場合も)はNaNに評価されます。
  • NaN == 0falseに評価されます。
  • したがって、return trueは実行されません。

それで、それは余分な繰り返しが決して起こらなかったかのようです - それは残りの論理に影響を及ぼしません。コードは、追加の反復がない場合と同じ結果を生成します。

しかしそこにたどり着くために、それは配列の終わりを越えて存在しない要素を読み込もうとしました。これは配列を最適化から追い出します - あるいは少なくともこの講演の時点ではそうでした。

<を使用した2番目のループは、配列内に存在する要素のみを読み取るため、最適化された配列とコードを使用できます。

この問題は講演の ページ90-91 で説明されていて、その前後のページで関連した議論があります。

私はたまたまこのGoogleのI/Oプレゼンテーションに出席し、その後スピーカー(V8の作者の一人)と話しました。ある特定の状況を最適化するための誤解を招いた(後見として)試みとして、私は自分自身のコードで配列の終わりを越えて読み取ることを含む手法を使用していました。彼は、配列の終わりを越えてreadを偶数にしようとすると、単純な最適化フォーマットが使用されなくなることを確認しました。

V8の作者が言ったことがまだ真実であるならば、それから配列の終わりを過ぎて読むことはそれが最適化されるのを防ぎ、そしてより遅いフォーマットにフォールバックする必要があるでしょう。

今のところ、このケースを効率的に処理するためにV8が改善されたか、または他のJavaScriptエンジンが別の方法でそれを処理する可能性があります。私はどちらにしてもそれについては知りませんが、この非最適化はプレゼンテーションが語っていたことです。

226
Michael Geary

それに科学性を追加するために、これがjsperfです。

https://jsperf.com/ints-values-in-out-of-array-bounds

整数で満たされた配列の制御ケースと、範囲内に留まりながらモジュラ演算を行うループをテストします。 5つのテストケースがあります。

  • 1.範囲外のループ
  • ホーリーアレイ
  • 3. NaNに対するモジュラ演算
  • 4.完全に未定義の値
  • 5. new Array()を使う

これは、最初の4つのケースが本当にパフォーマンスに悪いことを示しています。範囲外のループは他の3つよりも少し優れていますが、4つすべてがベストケースよりも約98%遅くなります。
new Array()のケースは、生の配列とほぼ同じくらい良く、わずか数パーセント遅くなります。

6
Nathan Adams