19

パフォーマンスが重要なコードがあり、関数の最初にスタックに異なるサイズの40個の配列のように割り当てる巨大な関数があります。これらのアレイのほとんどは、特定のアライメントを持っている必要があります(これらのアレイは、メモリアライメントを必要とするcpu命令を使用してチェーンのどこか別の場所にアクセスされるため(Intelおよびarm CPUの場合)。

gccの一部のバージョンは、スタック変数を適切に整列できない(特にarmコードの場合)ため、またはターゲットアーキテクチャの最大整列がコードが実際に要求するものよりも少ないと表示される場合もあるため、これらの配列を割り当てる以外に選択肢はありません。スタック上に配置し、手動で位置合わせします。

したがって、配列ごとに、適切に整列させるためにそのようなことを行う必要があります。

short history_[HIST_SIZE + 32];
short * history = (short*)((((uintptr_t)history_) + 31) & (~31));

このようにして、 history32バイト境界に位置合わせされます。40の配列すべてで同じことを行うのは面倒です。さらに、コードのこの部分はCPUを集中的に使用し、各配列に対して同じ配置手法を実行することはできません(この配置の混乱によりオプティマイザーが混乱し、レジスタ割り当てが異なると関数が大幅に遅くなります。 、より良い説明については、質問の最後にある説明を参照してください)。

だから...明らかに、私はその手動調整を一度だけ行い、これらのアレイが次々に配置されていると仮定したいと思います。また、これらの配列に追加のパディングを追加して、常に32バイトの倍数になるようにしました。したがって、スタック上にジャンボchar配列を作成し、これらすべての整列された配列を持つ構造体にキャストします。

struct tmp
{
   short history[HIST_SIZE];
   short history2[2*HIST_SIZE];
   ...
   int energy[320];
   ...
};


char buf[sizeof(tmp) + 32];
tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));

そんな感じ。おそらく最もエレガントではありませんが、それは本当に良い結果を生み出し、生成されたアセンブリの手動検査は、生成されたコードが多かれ少なかれ適切で許容できることを証明します。ビルドシステムが更新され、新しいGCCが使用されるようになり、突然、生成されたデータにアーティファクトが発生し始めました(たとえば、検証テストスイートからの出力は、asmコードが無効になっている純粋なCビルドでも少し正確ではなくなりました)。この問題のデバッグには長い時間がかかり、エイリアシングルールと新しいバージョンのGCCに関連しているように見えました。

それで、どうすればそれを成し遂げることができますか?標準ではない、移植性がない、未定義な​​どであると説明しようとして時間を無駄にしないでください(私はそれについて多くの記事を読みました)。また、コードを変更する方法はありません(問題を修正するために、GCCも変更することを検討しますが、コードをリファクタリングすることは考えません)...基本的に、新しいGCCに黒魔術の呪文を適用するだけです。最適化を無効にすることなく、このタイプのコードに対して機能的に同じコードを生成しますか?

編集:

  • このコードを複数のOS/コンパイラで使用しましたが、GCC4.6に基づく新しいNDKに切り替えると問題が発生し始めました。GCC 4.7(NDK r8dから)でも同じ悪い結果が得られます
  • 32バイトのアラインメントについて説明します。目を痛める場合は、他の好きな番号に置き換えてください。たとえば、役立つ場合は666に置き換えてください。ほとんどのアーキテクチャがその調整を必要としないことを言及する意味はまったくありません。スタック上で8KBのローカル配列を整列させると、16バイトの整列では15バイトが失われ、32バイトの整列では31バイトが失われます。私が何を意味するのかが明確であることを願っています。
  • パフォーマンスが重要なコードでは、スタック上に40個の配列があると言います。私はおそらく、それがうまく機能しているサードパーティの古いコードであり、それを台無しにしたくないと言う必要があります。それが良いか悪いかを言う必要はありません、そのためのポイントはありません。
  • このコード/関数は、十分にテストおよび定義された動作をしています。そのコードの要件の正確な数があります。たとえば、XkbまたはRAMを割り当て、Y kbの静的テーブルを使用し、最大Z kbのスタックスペースを消費します。コードは変更されないため、変更できません。
  • 「アライメントの混乱がオプティマイザを混乱させる」とは、各配列を個別にアライメントしようとすると、コードオプティマイザがアライメントコードに追加のレジスタを割り当て、コードのパフォーマンスが重要な部分に突然十分なレジスタがなくなり、代わりにスタックするためにトラッシングを開始することを意味します。その結果、コードの速度が低下します。この動作はARMCPUで観察されました(ちなみに、Intelについてはまったく心配していません)。
  • アーティファクトとは、出力が非ビット精度になることを意味し、ノイズが追加されます。このタイプエイリアシングの問題が原因であるか、コンパイラにバグがあり、最終的に関数からの出力が正しくなくなる可能性があります。

    要するに、質問のポイント...スタックスペースのランダムな量をどのように割り当てることができますか(char配列またはalloca、次に、そのスタックスペースへのポインタを整列し、このメモリチャンクを、構造自体が適切に整列されている限り、特定の変数の整列を保証する明確に定義されたレイアウトを持つ構造として再解釈します。私はあらゆる種類のアプローチを使用してメモリをキャストしようとしていますが、大きなスタック割り当てを別の関数に移動しますが、それでも出力が悪く、スタックが破損します。この巨大な関数がいくつかにヒットすることをますます考え始めています。 gccの一種のバグ。不思議なことに、このキャストをやっても、何をしようとしてもこのことができないのです。ちなみに、アライメントを必要とするすべての最適化を無効にしました。これは純粋なCスタイルのコードですが、それでも悪い結果が得られます(非ビット精度の出力と、ときどきスタックの破損がクラッシュします)。すべてを修正する簡単な修正です。代わりに次のように記述します。

    char buf[sizeof(tmp) + 32];
    tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));
    

    このコード:

    tmp buf;
    tmp * X = &buf;
    

    その後、すべてのバグが消えます!唯一の問題は、このコードが配列に対して適切な位置合わせを行わず、最適化を有効にするとクラッシュすることです。

    興味深い観察:
    このアプローチはうまく機能し、期待される出力を生成すると述べました。

    tmp buf;
    tmp * X = &buf;
    

    他のいくつかのファイルでは、その構造体tmp*にvoidポインターをキャストするだけのスタンドアロンのnoinline関数を追加しました。

    struct tmp * to_struct_tmp(void * buffer32)
    {
        return (struct tmp *)buffer32;
    }
    

    当初、to_struct_tmpを使用して割り当てられたメモリをキャストすると、gccをだまして期待どおりの結果が生成されると思いましたが、それでも無効な出力が生成されます。この方法で動作するコードを変更しようとすると、次のようになります。

    tmp buf;
    tmp * X = to_struct_tmp(&buf);
    

    それから私は同じ悪い結果を得る!うわー、他に何が言えますか?おそらく、厳密なエイリアシングルールに基づいて、gccは、to_struct_tmpから戻った直後に、tmp * X関連しておらず、未使用の変数としてtmp buf削除されていないと想定していますか?tmp bufまたは、予期しない結果をもたらす奇妙なことをします。また、生成されたアセンブリを検査しようとしましたが、関数に対して非常に異なるコードtmp * X = &buf;を生成するように変更すると、どういうわけか、そのエイリアシングルールがコード生成に大きな影響を与えます。tmp * X = to_struct_tmp(&buf);

    結論:
    あらゆる種類のテストを行った後、何を試しても機能しない可能性がある理由がわかりました。厳密な型エイリアシングに基づいて、GCCは静的配列が使用されていないと見なし、スタックを割り当てません。次に、スタックも使用するローカル変数が、tmp構造体が格納されているのと同じ場所に書き込まれます。言い換えれば、私のジャンボ構造体は、関数の他の変数と同じスタックメモリを共有します。これだけが、なぜそれが常に同じ悪い結果をもたらすのかを説明することができます。-fno-strict-aliasingは、この場合に予想されるように、問題を修正します。

  • 4

    4 に答える 4

    5

    まず、「標準違反」や「実装依存」などについて話題にしないようにお願いするときは、間違いなくあなたと一緒にいると言いたいです。あなたの質問は絶対に正当な私見です。

    すべての配列を1つにまとめるというあなたのアプローチstructも理にかなっています、それが私がすることです。

    どの「アーティファクト」を観察するかは、質問の定式化からは不明です。不要なコードが生成されていませんか?またはデータの不整合?後者の場合、(うまくいけば)STATIC_ASSERTコンパイル時に物事が適切に整列されていることを確認するためのようなものを使用できます。または、少なくともASSERTデバッグビルド時に実行時間を設定します。

    Eric Postpischilが提案したように、この構造をグローバルとして宣言することを検討できます(これがこの場合に当てはまる場合、マルチスレッドと再帰はオプションではないことを意味します)。

    もう1つ気づきたいのは、いわゆるスタックプローブです。単一の関数(正確には1ページ以上)でスタックから大量のメモリを割り当てる場合、一部のプラットフォーム(Win32など)では、コンパイラはスタックプローブと呼ばれる追加の初期化コードを追加します。これは、パフォーマンスにいくらかの影響を与える可能性もあります(ただし、マイナーである可能性があります)。

    また、40個の配列すべてを同時に必要としない場合は、それらの一部をに配置することができますunion。つまり、1つの大きなものがありstruct、その中にいくつかのサブstructsがグループ化されunionます。

    于 2013-01-05T13:31:40.497 に答える
    4

    ここには多くの問題があります。

    アラインメント: 32バイトのアラインメントを必要とするものはほとんどありません。16バイトのアライメントは、現在のIntelおよびARMプロセッサのSIMDタイプに役立ちます。現在のIntelプロセッサでAVXを使用している場合、16バイトで整列されているが32バイトで整列されていないアドレスを使用する場合のパフォーマンスコストは一般にわずかです。キャッシュラインを横切る32バイトのストアには大きなペナルティがある可能性があるため、32バイトのアラインメントが役立つ場合があります。それ以外の場合は、16バイトのアラインメントで問題ない場合があります。(OS XおよびiOSでは、malloc16バイトの整列メモリを返します。)

    重要なコードでの割り当て:パフォーマンスが重要なコードでのメモリの割り当ては避けてください。通常、メモリはプログラムの開始時、またはパフォーマンスクリティカルな作業を開始する前に割り当て、パフォーマンスクリティカルなコードで再利用する必要があります。パフォーマンスが重要なコードが始まる前にメモリを割り当てる場合、メモリの割り当てと準備にかかる時間は基本的に関係ありません。

    スタック上の大規模で多数のアレイ:スタックは大規模なメモリ割り当てを目的としておらず、その使用には制限があります。現在問題が発生していない場合でも、将来のコードの明らかに無関係な変更は、スタック上の大量のメモリの使用と相互作用し、スタックオーバーフローを引き起こす可能性があります。

    多数のアレイ: 40アレイがたくさんあります。これらがすべて同時に異なるデータに使用されている場合を除き、必然的に使用されている場合を除き、同じスペースの一部を異なるデータと目的に再利用するように努める必要があります。異なるアレイを不必要に使用すると、必要以上にキャッシュスラッシングが発生する可能性があります。

    最適化:「アライメントの混乱はオプティマイザを混乱させ、レジスタ割り当てが異なると関数が大幅に遅くなる」と言って意味が明確ではありません。関数内に複数の自動配列がある場合、アドレス演算によって配列からポインターを導出する場合でも、オプティマイザーはそれらが異なることを認識していると一般的に予想されます。たとえば、などのコードが与えられた場合、オプティマイザは、、、およびが異なる配列であることを認識していると予想されます。したがって、とa[i] = 3; b[i] = c[i]; a[i] = 4;同じにすることはできないため、を削除しても問題ありません。おそらくあなたが抱えている問題は、40個の配列で、配列への40個のポインターがあるので、コンパイラーがポインターをレジスターに出し入れすることになるということですか?abcc[i]a[i]a[i] = 3;

    その場合、複数の目的で再利用するアレイの数を減らすと、それを減らすのに役立つ場合があります。実際に一度に40個の配列を使用しているアルゴリズムがある場合は、一度に使用する配列が少なくなるようにアルゴリズムを再構築することを検討してください。アルゴリズムがメモリ内の40の異なる場所を指す必要がある場合、それらが割り当てられる場所や方法に関係なく、基本的に40のポインタが必要であり、40のポインタは使用可能なレジスタよりも多くなります。

    最適化とレジスタの使用について他に懸念がある場合は、それらについてより具体的にする必要があります。

    エイリアシングとアーティファクト:エイリアシングとアーティファクトの問題があると報告しましたが、それらを理解するのに十分な詳細を提供していません。charすべての配列を含む構造体として再解釈する1つの大きな配列がある場合、構造体内にエイリアシングはありません。したがって、どのような問題が発生しているのかは明確ではありません。

    于 2013-01-05T13:04:11.837 に答える
    3

    エイリアスベースの最適化を無効にして、それを1日と呼ぶだけです

    問題が実際に厳密なエイリアシングに関連する最適化によって引き起こされている場合は、問題-fno-strict-aliasingを解決します。さらに、その場合、定義上、これらの最適化はコードにとって安全ではなく、使用できないため、最適化が失われることを心配する必要はありません。

    プラエトリアニの良い点。gccでのエイリアス解析の導入によって引き起こされたある開発者のヒステリーを思い出します。あるLinuxカーネルの作者は、(A)エイリアスを作成し、(B)それでもその最適化を実現したいと考えていました。(それは過度に単純化されていますが-fno-strict-aliasing、問題を解決するようで、それほど費用はかからず、彼ら全員が他の魚を揚げていたに違いありません。)

    于 2013-01-06T00:40:59.000 に答える
    2

    32バイトの配置は、ボタンを押しすぎているように聞こえます。CPU命令では、これほど大きなアライメントは必要ありません。基本的に、アーキテクチャの最大のデータ型と同じ幅のアライメントで十分です。

    maxalign_tC11には、アーキテクチャの最大配置のダミータイプである概念foがあります。コンパイラにまだない場合は、次のような方法で簡単にシミュレートできます。

    union maxalign0 {
      long double a;
      long long b;
      ... perhaps a 128 integer type here ...
    };
    
    typedef union maxalign1 maxalign1;
    union maxalign1 {
      unsigned char bytes[sizeof(union maxalign0)];
      union maxalign0;
    }
    

    これで、プラットフォームの最大の配置があり、すべてのバイトがに設定されたデフォルトで初期化されるデータ型ができました0

    maxalign1 history_[someSize];
    short * history = history_.bytes;
    

    これにより、現在行っているひどいアドレス計算を回避できます。someSize常にの倍数を割り当てることを考慮に入れるために、を採用するだけで済みますsizeof(maxalign1)

    また、これにはエイリアシングの問題がないことを確認してください。まず最初にunionsこれのために作られたCで、次に(任意のバージョンの)文字ポインターは常に他のポインターをエイリアスすることができます。

    于 2013-01-05T08:54:05.183 に答える