パフォーマンスが重要なコードがあり、関数の最初にスタックに異なるサイズの40個の配列のように割り当てる巨大な関数があります。これらのアレイのほとんどは、特定のアライメントを持っている必要があります(これらのアレイは、メモリアライメントを必要とするcpu命令を使用してチェーンのどこか別の場所にアクセスされるため(Intelおよびarm CPUの場合)。
gccの一部のバージョンは、スタック変数を適切に整列できない(特にarmコードの場合)ため、またはターゲットアーキテクチャの最大整列がコードが実際に要求するものよりも少ないと表示される場合もあるため、これらの配列を割り当てる以外に選択肢はありません。スタック上に配置し、手動で位置合わせします。
したがって、配列ごとに、適切に整列させるためにそのようなことを行う必要があります。
short history_[HIST_SIZE + 32];
short * history = (short*)((((uintptr_t)history_) + 31) & (~31));
このようにして、 history
32バイト境界に位置合わせされます。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に黒魔術の呪文を適用するだけです。最適化を無効にすることなく、このタイプのコードに対して機能的に同じコードを生成しますか?
編集:
要するに、質問のポイント...スタックスペースのランダムな量をどのように割り当てることができますか(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は、この場合に予想されるように、問題を修正します。