11

gcc現在、 (テスト済みバージョン:4.8.4)で奇妙な効果が発生しています。

かなり高速に実行されるパフォーマンス指向のコードがあります。その速度は、多くの小さな関数をインライン化することに大きく依存します。

複数の.cファイルにまたがるインライン化は難しいため (-fltoまだ広く利用できるようにはなっていません)、多くの小さな関数 (通常はそれぞれ 1 行から 5 行のコード) を共通の C ファイルに保存し、そこにコーデックを開発しています。関連するデコーダー。私の標準では「比較的」大きいですが (約 2000 行ですが、多くはコメントと空白行だけです)、小さな部分に分割すると新しい問題が発生するため、可能であればそれを避けたいと思います。

Encoder と Decoder は逆の操作であるため、関連しています。しかし、プログラミングの観点から見ると、これらは完全に分離されており、いくつかの typedef と非常に低レベルの関数 (整列されていないメモリ位置からの読み取りなど) を除いて、共通点はありません。

奇妙な効果はこれです:

fnew最近、エンコーダ側に新しい機能を追加しました。新しい「入り口」です。ファイル内のどこからも使用されず、呼び出されません.c

それが存在するという単純な事実は、デコーダー機能のパフォーマンスをfdec大幅に低下させ、20% 以上低下させます。これは、無視するには多すぎます。

ここで、エンコード操作とデコード操作は完全に分離されており、ほとんど何も共有されておらず、いくつかのマイナーな操作typedef(u32などu16) と関連する操作 (読み取り/書き込み) を除いて覚えておいてください。

新しいエンコーディング関数fnewを として定義するstaticと、デコーダのパフォーマンスがfdec向上し、通常の状態に戻ります。fnewから呼び出されないので.c、存在しないのと同じだと思います (デッド コードの削除)。

static fnewがエンコーダ側から呼び出されるようになった場合、 のパフォーマンスはfdec引き続き強力です。

しかし、fnew変更されるとすぐに、fdecパフォーマンスが大幅に低下します。

変更がしきい値を超えたと仮定して、次のパラメーターfnewを増やしました: (デフォルトでは、その値は 40 であると想定されています) 。gcc--param max-inline-insns-auto=60fdec

fnewそして、このゲームは、小さな変更やその他の類似のもののたびに永遠に続くと思います。さらに微調整が必​​要です.

これは単純に奇妙です。fnewfunction の小さな変更が、まったく関係のない function に影響を与えるという論理的な理由はありませんfdec。これは、関係だけが同じファイルにあるはずです。

これまでのところ、私が考案できる唯一の暫定的な説明は、単に存在するだけで、に影響を与えるfnew何らかの種類の を越えるのに十分であるということです。1. 存在しない、2.しかしどこからも呼び出されない、3 . インライン化できるほど小さい。しかし、それは問題を隠しているだけです。新しい機能を追加できないということですか?global file thresholdfdecfnewstaticstatic

本当に、ネットのどこにも満足のいく説明が見つかりませんでした。

誰かがすでに同等の副作用を経験しており、その解決策を見つけているかどうか知りたいと思っていました.

[編集]

もっとクレイジーなテストに行きましょう。今、私はただ遊ぶためだけに、まったく役に立たない別の関数を追加しています。内容は厳密には のコピペですfnewが、明らかに関数名が違うので としましょうwtf

wtf存在する場合、fnewが静的であるかどうかも、 の値が何であるかも問題ではありませんmax-inline-insns-auto。 のパフォーマンスはfdec正常に戻ります。どこからもwtf使用も呼び出しもされていませんが... :'(

【編集2】inline指示 がありません。すべての機能は通常またはstatic. インライン化の決定は、これまでのところ問題なく機能しているコンパイラの領域内でのみ行われます。

[編集 3] Peter Cordes が示唆したように、この問題はインラインではなく、命令の配置に関係しています。新しい Intel CPU (Sandy Bridge 以降) では、ホット ループは 32 バイト境界に揃えることでメリットがあります。問題は、デフォルトでは、gccそれらを 16 バイト境界に揃えることです。これにより、前のコードの長さに応じて、50% の確率で適切な位置合わせが行われます。したがって、「ランダムに見える」問題を理解するのは困難です。

すべてのループがセンシティブなわけではありません。これはクリティカル ループでのみ重要であり、アライメントが理想的ではない場合に、ループの長さが 32 バイトの命令セグメントをさらに 1 つ超える場合にのみ重要です。

4

2 に答える 2

2

長い議論になったので、私のコメントを答えに変えます。議論の結果、パフォーマンスの問題はアラインメントに敏感であることがわかりました。

https://stackoverflow.com/tags/x86/infoには、いくつかのパフォーマンス チューニング情報へのリンクがあり、Intel の最適化ガイドや、Agner Fog の非常に優れた内容が含まれています。Agner Fog のアセンブリ最適化アドバイスの一部は、Sandybridge 以降の CPU には完全には適用されません。ただし、特定の CPU に関する低レベルの詳細が必要な場合は、microarch ガイドが非常に優れています。

少なくとも自分で試すことができるコードへの外部リンクがなければ、handwave しかできません。コードをどこにも投稿しない場合は、Linuxperfや Intel VTune などのプロファイリング / CPU パフォーマンス カウンター ツールを使用して、妥当な時間内にこれを追跡する必要があります。


チャットで、OP は他の誰かがこの問題を抱えていることを発見しましたが、コードが投稿されています。 これはおそらく、OP が見ているのと同じ問題であり、Sandybridge スタイルの uop キャッシュのコード アラインメントが重要になる主要な方法の 1 つです。

遅いバージョンでは、ループの途中に 32B 境界があります。境界の前に開始する命令は、5 uops にデコードされます。したがって、最初のサイクルでは、uop キャッシュが機能しmov/add/movzbl/movます。mov2 番目のサイクルでは、現在のキャッシュ ラインに 1 つの uopしか残っていません。次に、3 番目のサイクル サイクルは、ループの最後の 2 つの uops を発行しaddますcmp+ja

問題movは から始まり0x..ffます。32B 境界にまたがる命令は、開始アドレスの uop キャッシュライン (の 1 つ) に入ると思います。

高速バージョンでは、反復は発行に 2 サイクルしかかかりません。同じ最初のサイクル、次にmov / add / cmp+ja2 番目のサイクルです。

最初の 4 つの命令の 1 つが 1 バイト長かった場合 (たとえば、役に立たないプレフィックスまたは REX プレフィックスで埋められた場合)、問題はありません。mov32B 境界の後に開始され、次の uop キャッシュ ラインの一部になるため、最初のキャッシュ ラインの最後にオッド マン アウトはありません。

私の知る限り、アセンブリと逆アセンブリ出力のチェックは、同じ命令のより長いバージョンを使用して (Agner Fog の Optimizing Assembly を参照)、4 uops の倍数で 32B 境界を取得する唯一の方法です。編集中にアセンブルされたコードの配置を示す GUI は知りません。(そして明らかに、これを行うことは手書きの asm に対してのみ機能し、脆弱です。コードをまったく変更すると、手書きの配置が壊れます。)

これが、Intel の最適化ガイドが重要なループを 32B に揃えることを推奨している理由です。

アセンブラーが、先行する命令をより長いエンコーディングを使用してアセンブルし、特定の長さにパディングするように要求する方法を持っていれば、非常にクールです。おそらく.startencodealign/.endencodealign 32ディレクティブのペアで、ディレクティブ間のコードにパディングを適用して、32B 境界で終了するようにします。ただし、使い方を誤るとひどいコードになる可能性があります。


インライン化パラメーターを変更すると、関数のサイズが変更され、他のコードが 16B の倍数でオーバーラップします。これは、関数の内容を変更するのと同様の効果です。内容が大きくなり、他の関数の配置が変更されます。

私は、コンパイラーが常に関数が理想的な整列位置で開始することを確認し、noop を使用してギャップを埋めることを期待していました。

トレードオフがあります。すべての関数を 64B (キャッシュ ラインの開始) に揃えると、パフォーマンスが低下します。コード密度が低下し、命令を保持するためにより多くのキャッシュ ラインが必要になります。16B が適切です。これは、最新の CPU の命令フェッチ/デコード チャンク サイズだからです。

Agner Fogには、各マイクロアーチの低レベルの詳細があります。ただし、彼は Broadwell 用に更新していませんが、uop キャッシュはおそらく Sandybridge 以降変更されていません。ランタイムを支配するかなり小さなループが 1 つあると思います。最初に何を探すべきか正確にはわかりません。おそらく、「遅い」バージョンでは、コードの 32B ブロックの終わり近く (したがって、uop キャッシュラインの終わり近く) にいくつかの分岐ターゲットがあり、フロントエンドから出てくる 1 クロックあたりの uop が 4 よりも大幅に少なくなります。

「遅い」バージョンと「速い」バージョンのパフォーマンス カウンターを調べて (たとえば を使用perf stat ./cmd)、異なるものがないかどうかを確認します。たとえば、キャッシュ ミスがさらに多い場合は、スレッド間でキャッシュ ラインが誤って共有されていることを示している可能性があります。また、プロファイリングして、「遅い」バージョンに新しいホットスポットがあるかどうかを確認します。(例: perf record ./cmd && perf reportLinux の場合)。

「高速」バージョンは何 uops/クロックになりますか? 3 を超える場合は、アラインメントの影響を受けやすいフロントエンドのボトルネック (おそらく uop キャッシュ内) が問題である可能性があります。それか、L1 / uop-cache ミスのいずれかが、アライメントが異なると、コードが利用可能なよりも多くのキャッシュ ラインを必要とすることを意味します。

とにかく、これは繰り返します。プロファイラー/パフォーマンス カウンターを使用して、「遅い」バージョンにある新しいボトルネックを見つけますが、「速い」バージョンにはありません。次に、そのコード ブロックの逆アセンブルを確認するのに時間を費やすことができます。(gcc の asm 出力を見ないでください。最終的なバイナリの逆アセンブルでアライメントを確認する必要があります。) 16B と 32B の境界を見てください。おそらく、2 つのバージョン間で異なる場所にあると思われます。それが問題の原因です。

また、compare/jcc が 16B 境界を正確に分割すると、アラインメントによってマクロ融合が失敗する可能性があります。あなたの場合はそうではありませんが、関数は常に 16B の倍数に揃えられているためです。

re: アラインメントのための自動化ツール: いいえ、バイナリを見て、アラインメントについて有益なことを教えてくれるものは知りません。コードと一緒に 4 つの uops と 32B 境界のグループを表示し、編集時に更新するエディターがあればいいのにと思います。

Intel の IACAはループの分析に役立つ場合がありますが、IIRC は実行された分岐については認識していません。フロントエンドの洗練されたモデルがないと思います。これは、ミスアライメントがパフォーマンスを低下させる場合に明らかに問題になります。

于 2015-09-02T20:42:08.590 に答える
0

私の経験では、パフォーマンスの低下は、インライン最適化を無効にすることが原因である可能性があります。

「インライン」修飾子は、関数を強制的にインライン化することを示していません。関数をインライン化するためのヒントをコンパイラに提供します。そのため、コンパイラのインライン最適化の基準がコードの些細な変更では満たされない場合、インラインで変更された関数は通常、静的関数にコンパイルされます。

そして、ネストされたインライン最適化により、問題がより複雑になることがあります。次のように、インライン関数 fB を呼び出すインライン関数 fA がある場合:

inline void fB(int x, int y) {
    return x * y;
}

inline void fA() {
    for(int i = 0; i < 0x10000000; ++i) {
        fB(i, i+1);
    }
}

void main() {
    fA();
}

この場合、fA と fB の両方がインライン化されていると予想されます。ただし、インライン化の基準が満たされない場合、パフォーマンスは予測できません。つまり、fB についてはインライン展開を無効にするとパフォーマンスが大幅に低下しますが、fA についてはごくわずかな低下になります。そしてご存知のように、コンパイラの内部決定は非常に複雑です。

インライン化を無効にする理由 (インライン化関数のサイズ、.c ファイルのサイズ、ローカル変数の数など)。

実際、C# では、このパフォーマンスの低下を経験しています。私の場合、単純なインライン関数にローカル変数を 1 つ追加すると、パフォーマンスが 60% 低下します。

編集:

コンパイルされたアセンブリ コードを読み取ることで、何が起こるかを調べることができます。「インライン」で変更された関数への予期しない実際の呼び出しがあると思います。

于 2015-09-02T14:45:47.940 に答える