2015-10-29 14 views
12

私はループ中に4スレッドのループを実行していますが、ループ内で関数を評価してカウンタを増分しています。if文によるC++の大規模なパフォーマンスの低下

while(1) { 
    int fitness = EnergyFunction::evaluate(sequence); 

    mutex.lock(); 
    counter++; 
    mutex.unlock(); 
} 

私はこのループを実行すると、私は4つの実行中のスレッドに言ったように、私は毎秒〜20 000 000評価を取得します。

while(1) { 
    if (dist(mt) == 0) { 
     sequence[distDim(mt)] = -1; 
    } else { 
     sequence[distDim(mt)] = 1; 
    } 
    int fitness = EnergyFunction::evaluate(sequence); 

    mainMTX.lock(); 
    overallGeneration++; 
    mainMTX.unlock(); 
} 

シーケンスにランダムな突然変異を追加すると、1秒あたり〜13,000,000回の評価が発生します。

while(1) { 
    if (dist(mt) == 0) { 
     sequence[distDim(mt)] = -1; 
    } else { 
     sequence[distDim(mt)] = 1; 
    } 
    int fitness = EnergyFunction::evaluate(sequence); 

    mainMTX.lock(); 
    if(fitness < overallFitness) 
     overallFitness = fitness; 

    overallGeneration++; 
    mainMTX.unlock(); 
} 

しかし、それは、新しいフィットネスと古いフィットネスを置き換える真であれば、新たなフィットネスが古いフィットネスよりも小さい場合には、ステートメントをチェックしている場合、私はシンプル追加。

しかし、パフォーマンスの損失は巨大です!今は毎秒20,000件の評価があります。ランダム突然変異部分を削除した場合、1秒あたり〜20,000回の評価も得られます。私は、このような大きなパフォーマンス損失の問題が何であるかを考え出すトラブルを抱えています

extern int overallFitness; 

として

変数overallFitnessが宣言されています。このような時間を2つのintと比較していますか?

また、私はそれがミューテックスロックに関係しているとは思いません。

UPDATE

このパフォーマンスの損失があるため、分岐予測のではなかったが、コンパイラは、単にこの呼び出しint fitness = EnergyFunction::evaluate(sequence);を無視していました。

は今、私はもう呼び出しを無視しない volatileおよびコンパイラを追加しました。

はまたそれらについて知らなかった、分岐予測ミスとatomic<int>を指摘ありがとうございました!

私はまた、ミューテックスの一部を除去するので、最終的なコードは次のようになり、原子のため、次

while(1) { 
    sequence[distDim(mt)] = lookup_Table[dist(mt)]; 
    fitness = EnergyFunction::evaluate(sequence); 
    if(fitness < overallFitness) 
     overallFitness = fitness; 
    ++overallGeneration; 
} 

は、今私は毎秒〜25の000評価を得ています。

+7

考えられる理由の1つは、[間違った予測です](http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array) –

+3

@R_Kapp Nah 、それは無関係です。これは単純にロックの競合です。 –

+5

'atomic 'が役に立ちます。 – Jarod42

答えて

31

プロファイラーを実行して、このページの下部に移動する必要があります。 Linuxでは、perfを使用してください。

私の推測では、最初の例では結果を使用しないため、EnergyFunction::evaluate()は完全に最適化されています。したがって、コンパイラはすべてを破棄することができます。戻り値をvolatile変数に書き込むことができます。コンパイラまたはリンカーは呼び出しを最適化しないように強制する必要があります。 1000倍のスピードアップは間違いなく単純な比較に起因するものではありません。

+1

これはおそらく、O1以上のgccがないとgccがその関数呼び出しをただちに最適化する理由です。 – fireant

+0

良いキャッチマーク! –

+1

@Peterピーターは申し訳ありません。私はちょうどあなたの答えを逃した:)私は私の答えのクレジットにもあなたを追加します。とにかくいいキャッチ! –

11

それがなかった場合、私は驚かれることでしょうaltoughとてもスマートコンパイラは、完全にミューテックスを削除することができるかもしれ1.でINTを高めるためにアトミック命令が実際にあります。これをテストするには、アセンブリを調べるか、またはmutexを削除し、overallGenerationの型をatomic<int>に変更して、それがまだどれくらい速いかを調べます。最後の遅い例では、この最適化はもはや不可能です。

また、evaluateがグローバル状態に対して何も実行せず、結果が使用されていないことがコンパイラによって確認された場合は、evaluateへの呼び出し全体をスキップできます。その場合、アセンブリを見たり、EnergyFunction::evaluate(sequence)への呼び出しを取り除いてタイミングを調べたりします。高速化しなければ、関数は最初に呼び出されませんでした。最後の遅い例では、この最適化はもはや不可能です。あなたは別のオブジェクトファイル(他のcppまたはライブラリ)に関数を定義し、リンク時の最適化を無効にすることでEnergyFunction::evaluate(sequence)を実行していないから、コンパイラを停止することができるはずです。

パフォーマンスの差を生み出す他の効果もありますが、要因1000の違いを説明できる他の効果はありません。要因1000は、通常、以前のテストで気になっていたコンパイラと変更を意味しますそれを不正行為から防ぐ。

+0

変数を更新するには、ローカルのコアキャッシュを無効にしてすべてのコアに値をブロードキャストする必要があるため、アトミック命令はここでは役に立ちません(少なくとも基本的ではありません)。そしてそれはまだ非効率的です(私が正しく覚えていれば、およそローカル変数の読み込みよりも1000倍遅い)。 –

+0

@KonradRudolph原子インクリメント命令は、すでにロックされているmutex(おそらく原子的なcompareexchangeであり、それに続くカーネル呼び出し)を待つよりも潜在的に規模が安いです。これはmutexの実装に基づいて異なります。問題は、コンパイラがメモリバリアを中心に最適化することに非常に注意しているので、mutexコードに触れることはほとんどありません。したがって私が提案した第二の選択肢は、より可能性の高い原因です。 – Peter

+2

これは間違いありませんが、このコードがスレッド間にこの依存関係を持っているという事実は、私の意見ではもっと大きな問題であり、アルゴリズム的に解決されるべきです。つまり、原子操作のために単一のミューテックスを取り除くのではなく、コードを再構築することで、潜在的にはるかに多くのコードを保存することができます。あなたはあなたが何をしているのか分かっているようですが、実際には非常に安いミューテックスと同程度の場合、多くの人がアトミック操作で魔法を実行すると期待しています。彼らは実際には計算上の依存関係を修正するものではありません。 –

7

私の答えがそのような劇的な性能低下についての説明を与えることは確かではありませんが、確かにそれに影響を与える可能性があります。

if (dist(mt) == 0) { 
    sequence[distDim(mt)] = -1; 
} else { 
    sequence[distDim(mt)] = 1; 
} 

この場合、CPU(少なくとも、IA)は分岐予測を行い、そこに分岐ミス予測の場合します:あなたは非クリティカル地域に支店を追加しました最初のケ​​ースで

パフォーマンスペナルティです - これは既知の事実です。

秒ほかについて、あなたはクリティカルエリアへの分岐を追加しました:その順番に、「ミス・予測」のペナルティに加えて、あるコードの量を増加

mainMTX.lock(); 
if(fitness < overallFitness) 
    overallFitness = fitness; 

overallGeneration++; 
mainMTX.unlock(); 

その領域で実行され、したがって他のスレッドがmainMTX.unlock();を待たなければならない確率。

NOTE

すべてのグローバル/共有リソースがvolatileのように定義されていることを確認してください。さもなければ、コンパイラはそれらを最適化することができます(これは、最初の評価のような高い数を説明するかもしれません)。

overallFitnessの場合、externと宣言されているため、おそらく最適化されませんが、は最適化されている可能性があります。この場合、重要なエリアに「実際の」メモリアクセスを追加した後、このパフォーマンス低下を説明することがあります。

注2

私はまだ私が提供説明は、このような大幅なパフォーマンスの低下を説明できることを確認していません。だから、あなたが投稿しなかったコードに実装の詳細がいくつかあると思います(たとえばvolatileなど)。性能低下の原因ということ

EDIT

としてピーター(@Peter)マークLakata(@MarkLakata)は、別の回答で述べた、と私は彼らに同意する傾向があり、最も可能性が高いです最初のケースではfitnessが使用されていないので、コンパイラはその変数を関数呼び出しと共に最適化しました。 2番目のケースではfitnessが使用されたので、コンパイラはそれを最適化しませんでした。ピーターとマークはいいキャッチ!私はその点を見逃してしまった。

+0

"すべてのグローバル/共有リソースが' volatile'として定義されていることを確認してください。 "ミューテックスは既にそれを世話しなければなりません、それはメモリの壁を導入します。 – JAB

+0

@JABいいえ、mutexは共有リソースのみを保護しますが、コンパイラは、たとえばこのリソースをメモリからレジスタに移動し、代わりにそのリソースで動作させることができます。 Volatileは、変数を最適化しないようにコンパイラに指示し、アクセスするたびにメモリ上でそのアクセスを実行する必要があります。 –

2

私はこれが厳密に質問に対する回答ではなく、問題の代替案として提示されていることを認識しています。

コード実行中にoverallGenerationが使用されていますか?つまり、いつ計算を停止するかを決定するために使用されますか? ではない場合は、グローバルカウンタを同期させずに、スレッドごとにカウンタを設定し、計算が完了した後、すべてのスレッドごとのカウンタを合計して合計にすることができます。同様にoverallFitnessの場合は、スレッドごとにmaxFitnessを追跡し、計算が終了すると4つの結果の最大値を選択できます。

スレッド同期をまったく持たないと、CPU使用率は100%になります。

関連する問題