web-dev-qa-db-ja.com

この関数の実行速度が遅くなるのはなぜですか?

関数内のローカル変数がスタックに格納されているかどうかを確認するための実験を試みてきました。

だから私は少しのパフォーマンステストを書きました

function test(fn, times){
    var i = times;
    var t = Date.now()
    while(i--){
        fn()
    }
    return Date.now() - t;
} 
ene
function straight(){
    var a = 1
    var b = 2
    var c = 3
    var d = 4
    var e = 5
    a = a * 5
    b = Math.pow(b, 10)
    c = Math.pow(c, 11)
    d = Math.pow(d, 12)
    e = Math.pow(e, 25)
}
function inversed(){
    var a = 1
    var b = 2
    var c = 3
    var d = 4
    var e = 5
    e = Math.pow(e, 25)
    d = Math.pow(d, 12)
    c = Math.pow(c, 11)
    b = Math.pow(b, 10)
    a = a * 5
}

私は、逆関数の動作がはるかに速くなると期待していました。代わりに、驚くべき結果が出ました。

関数の1つをテストするまで、2番目の関数をテストした後よりも10倍速く実行されます。

例:

> test(straight, 10000000)
30
> test(straight, 10000000)
32
> test(inversed, 10000000)
390
> test(straight, 10000000)
392
> test(inversed, 10000000)
390

別の順序でテストしても同じ動作になります。

> test(inversed, 10000000)
25
> test(straight, 10000000)
392
> test(inversed, 10000000)
394

ChromeブラウザーとNode.jsの両方でテストしましたが、なぜそれが起こるのか全く分かりません。この効果は、現在のページを更新するかNode REPLを再起動するまで続きます。

そのような重要な(〜12倍悪い)パフォーマンスの原因は何でしょうか?

PS。一部の環境でのみ機能するようですので、テストに使用している環境を記述してください。

私のものは:

OS:Ubuntu 14.04
Node v0.10.37
Chrome 43.0.2357.134(公式ビルド)(64ビット)

/編集
Firefox 39では、順序に関係なく、テストごとに約5500ミリ秒かかります。特定のエンジンでのみ発生するようです。

/ Edit2
関数をテスト関数にインライン化すると、常に同じ時間で実行されます。
関数パラメーターが常に同じ関数である場合、関数パラメーターをインライン化する最適化がある可能性はありますか?

64
Krzysztof Wende

testを2つの異なる関数fn()で呼び出すと、その内部の呼び出しサイトはメガモーフィックになり、V8はインライン化できません。

V8の関数呼び出し(メソッド呼び出しo.m(...)とは対照的に)には、真のポリモーフィックインラインキャッシュの代わりにone elementインラインキャッシュが伴います。

V8はfn()呼び出しサイトでインライン化できないため、さまざまな最適化をコードに適用できません。 IRHydra (利便性のためにGistにコンパイルアーティファクトをアップロードしました)のコードを見ると、testの最初の最適化バージョン(fn = straight)完全に空のメインループがあります。

enter image description here

V8は、straightremovedをインライン化し、Dead Code Elimination最適化でベンチマークしたいすべてのコードをインライン化しました。 DCE V8の代わりにV8の古いバージョンでは、LICMを介してループからコードを引き上げるだけです。コードは完全にループ不変であるためです。

straightがインライン化されていない場合、V8はこれらの最適化を適用できません-したがってパフォーマンスの違いです。 V8の新しいバージョンでは、straightおよびinversed自体にDCEが適用され、それらは空の関数になります。

enter image description here

そのため、パフォーマンスの差はそれほど大きくありません(約2〜3倍)。古いV8はDCEで十分に攻撃的ではありませんでした-インラインケースのピークパフォーマンスは積極的なループ不変コードモーション(LICM)の結果だけであったため、インラインケースと非インラインケースの大きな違いに現れます。

関連するメモでは、これはベンチマークがこのように書かれてはならない理由を示しています-結果は空のループを測定することになり、役に立たないからです。

ポリモーフィズムとV8でのその意味に興味がある場合は、私の投稿を確認してください 「単相性の問題」 (「すべてのキャッシュが同じではない」セクションでは、関数呼び出しに関連するキャッシュについて説明します)。また、マイクロベンチマークの危険性に関する私の講演の1つを読むことをお勧めします。最新の "Benchmarking JS" GOTO Chicago 2015からの講演( video )-よくある落とし穴を避けるのに役立つかもしれません。

102

あなたはスタックを誤解しています。

「実際の」スタックには実際にPushおよびPop操作しかありませんが、これは実行に使用される種類のスタックには実際には適用されません。 PushPop以外に、アドレスがある限り、任意の変数にランダムにアクセスすることもできます。これは、コンパイラーがローカルの順序を変更しなくても、ローカルの順序は重要ではないことを意味します。疑似アセンブリーでは、

var x = 1;
var y = 2;

x = x + 1;
y = y + 1;

のようなものに翻訳します

Push 1 ; x
Push 2 ; y

; get y and save it
pop tmp
; get x and put it in the accumulator
pop a
; add 1 to the accumulator
add a, 1
; store the accumulator back in x
Push a
; restore y
Push tmp
; ... and add 1 to y

実際、実際のコードは次のようになります。

Push 1 ; x
Push 2 ; y

add [bp], 1
add [bp+4], 1

スレッドスタックが実際に厳密なスタックである場合、これは不可能です。その場合、操作とローカルの順序は、現在よりもはるかに重要になります。代わりに、スタック上の値へのランダムアクセスを許可することにより、コンパイラとCPUの両方の多くの作業を節約できます。

あなたの実際の質問に答えるために、私はどちらの機能も実際には何もしないと疑っています。あなたはローカルを変更するだけで、関数は何も返しません-コンパイラが関数本体を完全にドロップすることは完全に合法であり、場合によっては関数呼び出しさえもです。そうだとすれば、観察しているパフォーマンスの違いは、おそらく単なる測定結果、または関数の呼び出しや反復処理に固有のコストに関係するものです。

17
Luaan

関数をテスト関数にインライン化すると、常に同じ時間で実行されます。
関数パラメーターが常に同じ関数である場合、関数パラメーターをインライン化する最適化がある可能性はありますか?

はい、これはまさにあなたが観察しているもののようです。 @Luaanで既に述べたように、コンパイラは副作用を持たず、一部のローカル変数のみを操作するため、straightおよびinverse関数の本体を削除する可能性があります。

test(…, 100000)を初めて呼び出すとき、最適化コンパイラは、いくつかの反復の後、呼び出されるfn()が常に同じであると認識し、インラインで実行し、コストのかかる関数呼び出しを回避します。現在行われているのは、変数を1000万回デクリメントし、0

ただし、異なるtestfnを呼び出す場合は、最適化を解除する必要があります。後で他の最適化を再度行う場合がありますが、呼び出される2つの異なる関数があることを知ったため、それらをインライン化することはできません。

実際に測定しているのは関数呼び出しだけなので、結果に重大な違いが生じることになります。

関数内のローカル変数がスタックに保存されているかどうかを確認する実験

あなたの実際の質問に関しては、いいえ、単一の変数はスタック( スタックマシン )ではなく、レジスタ( レジスタマシン )に保存されます。関数で宣言または使用される順序は関係ありません。

しかし、それらは、いわゆる「スタックフレーム」の一部として、 stack に保存されます。関数呼び出しごとに1つのフレームがあり、その実行コンテキストの変数が保存されます。あなたの場合、スタックは次のようになります。

[straight: a, b, c, d, e]
[test: fn, times, i, t]
…
3
Bergi