TL; DR遅いループは、配列の「範囲外」へのアクセスが原因です。これにより、エンジンは、最適化をほとんどまたはまったく行わずに関数を再コンパイルするか、これらの最適化のいずれかで関数をコンパイルしなくなります( (JIT-)コンパイラが最初のコンパイル「バージョン」の前にこの状態を検出/疑った場合)、以下の理由を読んでください。
誰か
がこれを言わ
なければならない(まったく驚いていない):
OPのスニペットは、JavaScriptの「配列」が索引付けされていることを概説/強調することを意図した初心者プログラミング本のデファクトの例であることがかつてあった1ではなく0として、一般的な「初心者の間違い」の例として使用します(「プログラミングエラー」というフレーズをどのように回避したか気になりませんか
;)
):
範囲外の配列アクセス。
実施例1:(常にES262で)0ベースのインデックスを使用して、5つの要素の(インデックス間のギャップなしに連続している(手段)と、各インデックスに実際の要素)。
Dense Array
var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
// indexes are: 0 , 1 , 2 , 3 , 4 // there is NO index number 5
したがって、実際には<
vs <=
(または「1つの追加の反復」)間のパフォーマンスの違いについて話しているわけではありません
が、「正しいスニペット(b)が誤ったスニペット(a)よりも高速に実行されるのはなぜですか」
答えは2つあります(ただし、ES262言語の実装者から見ると、どちらも最適化の形式です)。
- データ表現:配列をメモリ内で内部的に表現/格納する方法(オブジェクト、ハッシュマップ、「実際の」数値配列など)
- 関数型マシンコード:これらの「配列」にアクセス/処理(読み取り/変更)するコードをコンパイルする方法
項目1は受け入れられた回答で十分に(そして正しくは私見で)説明されていますが、項目2のコンパイルに 2ワード(「コード」)しか費やしていません。
より正確には、JITコンパイル、さらに重要なJIT- REコンパイル!
言語仕様は、基本的には一連のアルゴリズム(「定義された最終結果を達成するために実行する手順」)の単なる説明です。結局のところ、これは言語を記述する非常に美しい方法です。そして、エンジンが指定された結果を達成するために使用する実際の方法を実装者に任せ、定義された結果を生成するためのより効率的な方法を考え出す十分な機会を与えます。仕様適合エンジンは、定義された入力に対して仕様適合結果を提供する必要があります。
現在、JavaScriptコード/ライブラリ/使用量が増加し、「実際の」コンパイラが使用するリソース(時間/メモリなど)の量を記憶しているため、Webページにアクセスするユーザーをそれほど長く待てない(そしてそれらを要求する)ことができないことは明らかです。多くのリソースを利用できるようにするためです)。
次の単純な関数を想像してみてください。
function sum(arr){
var r=0, i=0;
for(;i<arr.length;) r+=arr[i++];
return r;
}
完全にクリアですよね?追加の説明は必要ありませんよね?戻り値の型はNumber
、ですよね?
まあ..いいえ、いいえ&いいえ...それはあなたが名前付き関数パラメータに渡す引数に依存しますarr
...
sum('abcde'); // String('0abcde')
sum([1,2,3]); // Number(6)
sum([1,,3]); // Number(NaN)
sum(['1',,3]); // String('01undefined3')
sum([1,,'3']); // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]); // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]); // Number(8)
問題を見ますか?次に、これが可能性のある膨大な順列をかろうじて削っているだけであると考えます...完了するまで、関数RETURNがどのような種類のTYPEであるかさえわかりません...
今度は、この同じ関数コードが、文字どおり(ソースコードで)完全に記述され、プログラム内で動的に生成された「配列」の両方で、さまざまなタイプまたは入力のバリエーションで実際に使用されていると想像してください。
したがって、関数sum
JUST ONCE をコンパイルする場合、すべてのタイプの入力に対して常に仕様定義の結果を返す唯一の方法は、明らかに、すべての仕様規定のメインANDサブステップを実行することによってのみ、仕様に準拠した結果を保証できます。 (名前のないpre-y2kブラウザのように)。最適化(仮定がないため)とデッドスローインタープリタードスクリプト言語は残っていません。
JITコンパイル(ジャストインタイムのJIT)は現在人気のあるソリューションです。
したがって、関数の動作、戻り、受け入れに関する仮定を使用して、関数のコンパイルを開始します。
(予期しない入力を受け取ったなどの理由で)関数が仕様に準拠していない結果を返し始める可能性があるかどうかを検出するために、可能な限り簡単なチェックを考え出します。次に、以前のコンパイル結果を捨てて、より精巧なものに再コンパイルし、すでに持っている部分的な結果をどうするかを決定し(信頼できるか、確実に再計算できるか)、関数をプログラムに結び付け、再試行。最終的には仕様のように段階的なスクリプト解釈にフォールバックします。
これにはすべて時間がかかります!
すべてのブラウザがそれぞれのエンジンで動作します。サブバージョンごとに、物事が改善され、後退します。文字列は歴史のある時点で本当に不変の文字列であったため(つまり、array.joinは文字列の連結よりも高速でした)、問題を緩和するロープ(または類似のもの)を使用します。どちらも仕様に準拠した結果を返し、それが重要です!
簡単に言えば、JavaScriptの言語のセマンティクスが(OPの例のこのサイレントバグのように)しばしば後戻りしたからといって、「愚かな」ミスがコンパイラが高速のマシンコードを吐き出す可能性が高まるということではありません。これは、「通常」正しい指示を書いたことを前提としています:(プログラミング言語の)「ユーザー」が持っている必要のある現在のマントラは、コンパイラを支援し、必要なものを説明し、一般的なイディオムを支持します(基本的な理解のためにasm.jsからヒントを取得します)どのブラウザが最適化を試みることができるか、およびその理由)。
このため、パフォーマンスについて話すことは両方とも重要ですが、地雷原もです(そして、地雷原のため、私は本当にいくつかの関連資料を指して(そして引用して)終わらせたいと思います:
存在しないオブジェクトプロパティや範囲外の配列要素にアクセスするとundefined
、例外が発生する代わりに値が返されます。これらの動的機能により、JavaScriptでのプログラミングが便利になりますが、JavaScriptを効率的なマシンコードにコンパイルすることも困難になります。
...
効果的なJIT最適化の重要な前提は、プログラマーがJavaScriptの動的機能を体系的に使用することです。たとえば、JITコンパイラは、オブジェクトプロパティが特定のタイプのオブジェクトに特定の順序で追加されることが多い、または範囲外の配列アクセスがほとんど発生しないという事実を利用しています。JITコンパイラーは、これらの規則性の仮定を利用して、実行時に効率的なマシンコードを生成します。コードブロックが前提条件を満たしている場合、JavaScriptエンジンは効率的な生成されたマシンコードを実行します。それ以外の場合、エンジンはより遅いコードまたはプログラムの解釈にフォールバックする必要があります。
出典:
"JITProf:Pinpointing JIT-unfriendly JavaScript Code"
Berkeley出版物、2014年、Liang Gong、Michael Pradel、Koushik Sen著
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS(境界外の配列アクセスも望まない):
事前コンパイル
asm.jsはJavaScriptの厳密なサブセットであるため、この仕様では検証ロジックのみが定義されています。実行セマンティクスはJavaScriptの単純なものです。ただし、検証済みのasm.jsは、事前(AOT)コンパイルに適しています。さらに、AOTコンパイラーによって生成されるコードは非常に効率的で、以下の機能を備えています。
- 整数および浮動小数点数のボックス化されていない表現。
- ランタイム型チェックがない;
- ガベージコレクションがないこと。そして
- 効率的なヒープのロードとストア(実装戦略はプラットフォームによって異なる)。
検証に失敗したコードは、解釈やジャストインタイム(JIT)コンパイルなどの従来の手段による実行にフォールバックする必要があります。
http://asmjs.org/spec/latest/
そして最後にhttps://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/
境界を削除するときのエンジンの内部パフォーマンスの改善に関する小さなサブセクションがありました-チェック(境界チェックをループの外に引き上げるだけで、既に40%の改善がありました)。
編集:
複数のソースがJIT再コンパイルのさまざまなレベルから解釈まで話していることに注意してください。
OPのスニペットに関する上記の情報に基づく理論的な例:
- isPrimeDivisibleの呼び出し
- 一般的な仮定(境界外のアクセスがないなど)を使用してisPrimeDivisibleをコンパイルします。
- 仕事する
- BAM、突然配列のアクセスが範囲外になりました(最後にあります)。
- クラップ、エンジンは言う、異なる(より少ない)仮定を使用してisPrimeDivisibleを再コンパイルしてみましょう。このサンプルエンジンは、現在の部分的な結果を再利用できるかどうかを判断しようとしません。
- 遅い関数を使用してすべての作業を再計算します(うまくいけばそれは終了します。それ以外の場合は繰り返し、今回はコードを解釈します)。
- 結果を返す
したがって、その後の時間は
次のとおりです。最初の実行(最後に失敗)+すべての作業を繰り返しごとに遅いマシンコードを使用してもう一度行う+再コンパイルなどは、この理論的な例では明らかに2倍以上かかります !
編集2:( 免責事項:以下の事実に基づく推測)
私が考えれば考えるほど、この回答は、誤ったスニペットa(またはスニペットbのパフォーマンスボーナス)に対するこの「ペナルティ」の主な理由を実際に説明できると思う、あなたの考え次第で)正確になぜ私がそれを(スニペットa)プログラミングエラーと呼ぶのを恐れているのか:
これthis.primes
が「密配列」の純粋な数値であると仮定するのはかなり魅力的です。
- ソースコード内のハードコードされたリテラル(コンパイル前にすべてがコンパイラに認識されているため、「実際の」配列になることが知られている優れた候補)または
- 最も可能性が高いのは、事前にサイズ設定された(
new Array(/*size value*/)
)を昇順で埋める数値関数を使用して生成されたものです(「本物の」配列になる別の長い間知られている候補)。
我々はまた、ことを知ってprimes
、配列の長さがされ、キャッシュされたようprime_count
!(意図と固定サイズを示します)。
また、ほとんどのエンジンは最初に配列を(必要なときに)コピーオンモディファイとして渡すため、(変更しない場合)配列の処理がはるかに高速になります。
したがって、Array primes
はおそらく作成後に変更されない内部で最適化された配列である可能性が最も高く(作成後に配列を変更するコードがない場合はコンパイラーに簡単にわかる)、したがってすでに(該当する場合)かなり多くのエンジンが)、最適化された形で保存されているかのようでしたTyped Array
。
sum
関数の例で明確にしようとしたように、渡される引数は、実際に何が必要か、その特定のコードがマシンコードにコンパイルされる方法に大きく影響します。関数にa String
を渡してsum
も文字列は変更されませんが、関数のJITコンパイル方法が変更されます。配列をに渡すと、sum
マシンコードの異なる(おそらく、このタイプの追加、または渡されたオブジェクトの「シェイプ」)バージョンがコンパイルされます。
Typed_Arrayのようなprimes
配列をオンザフライでsome_elseに変換するのは少しおかしく思えますが、コンパイラーはこの関数がそれを変更することさえないことを知っています!
2つのオプションを残すこれらの仮定の下:
- 範囲外を想定して数値クランチャーとしてコンパイルし、最後に範囲外の問題に遭遇し、再コンパイルしてやり直します(上記の編集1の理論例で概説)
- コンパイラはすでに範囲外のアクセスを事前に検出(または疑っていますか?)し、関数は、渡された引数がスパースオブジェクトであるかのようにJITコンパイルされ、その結果、機能的なマシンコードが遅くなります(チェック/変換/強制が増えるため)等。)。言い換えると、関数は特定の最適化に適格ではなかったため、「スパース配列」(のような)引数を受け取ったかのようにコンパイルされました。
私は今、これらの2つのうちのどれがそれであるのか本当に疑問に思います!
<=
と<
理論的には、すべての最新のプロセッサ(通訳)での実際の実装の両方で、同じです。