Cコンパイラはスタック変数を再配置できますか?
-
04-07-2019 - |
質問
過去に組み込みシステムのプロジェクトに取り組んでおり、スタック変数の宣言の順序を変更して、結果の実行可能ファイルのサイズを小さくしました。たとえば、次の場合:
void func()
{
char c;
int i;
short s;
...
}
次のように並べ替えます:
void func()
{
int i;
short s;
char c;
...
}
アライメントの問題により、最初のスタックでは12バイトのスタックスペースが使用され、2番目のスタックでは8バイトしか使用されませんでした。
これはCコンパイラの標準的な動作ですか、それとも使用していたコンパイラの欠点ですか?
コンパイラは、必要に応じて実行可能ファイルのサイズを小さくするために、スタック変数を並べ替えることができるように思えます。 C標準のいくつかの側面がこれを防ぐことが示唆されていますが、どちらにしても信頼できるソースを見つけることができませんでした。
ボーナス質問として、これはC ++コンパイラにも適用されますか?
編集
答えが「はい」の場合、C / C ++コンパイラはスタック変数を再配置できます。これを確実に行うコンパイラの例を挙げてください。コンパイラのドキュメントまたはこれを裏付ける類似のドキュメントをご覧ください。
再度編集
皆さんのご協力に感謝します。ドキュメントについては、 GCCでの最適なスタックスロット割り当て(pdf)、Naveen SharmaおよびSanjiv Kumar Guptaによる、2003年のGCCサミット会議で発表されました。
ここで問題になっているプロジェクトは、ARM開発にADSコンパイラを使用していました。 ARM-Thumbアーキテクチャがローカルスタックフレームでアドレスを計算する方法のために、上記のような順序宣言を使用すると、パフォーマンスとスタックサイズを改善できることがコンパイラーのドキュメントで言及されています。そのコンパイラは、これを利用するためにローカルを自動的に再配置しませんでした。ここにリンクされている論文は、2003年の時点でGCCはスタックフレームを再配置してARM-Thumbプロセッサの参照の局所性を改善しなかったと述べていますが、それはあなたができることを意味します。
これがGCCで実装されたことを明確に示すものは見つかりませんが、この論文はあなたがすべて正しいという証拠としてカウントされると思います。どうもありがとう。
解決
標準にはCまたはC ++コンパイラーを禁止するものは何もないので、はい、コンパイラーはそれを行うことができます。
相対的な順序を維持する必要がある集合体(つまり、構造体)では異なりますが、コンパイラーは望ましいアライメントを実現するためにパッドバイトを挿入する場合があります。
IIRCの新しいMSVCコンパイラは、ローカルのバッファオーバーフローに対する戦いでその自由を使用します。
補足として、C ++では、コンパイラがメモリレイアウトを並べ替える場合でも、破棄の順序は宣言の逆の順序でなければなりません。
(ただし、章と詩を引用することはできませんが、これはメモリからのものです。)
他のヒント
コンパイラーは、ローカル変数のスタックレイアウトを並べ替えるだけでなく、それらをレジスターに割り当てたり、レジスターやスタック上でライブに割り当てたり、2つのローカルをメモリ内の同じスロットに割り当てることができます(その有効範囲は重複していません)、変数を完全に排除することさえできます。
スタックは存在する必要さえありません(実際、C99標準には「スタック」という単語が1つしかありません)。そのため、コンパイラーは、変数のセマンティクスを自動ストレージ期間で保持する限り、何でも自由に実行できます。
例として:レジスターに保存されているため、デバッガーでローカル変数を表示できない状況に何度も遭遇しました。
コンパイラーは、変数のアドレスが取得/使用されていないことが分析によって示された場合にのみ、スタックから変数を削除して登録することもできます。
コンパイラは、データ用にスタックをまったく使用していません。プラットフォームが非常に小さいため、8バイトと12バイトのスタックを心配している場合は、かなり特殊なアプローチを持つコンパイラーが存在する可能性があります。 (いくつかのPICおよび8051コンパイラが思い浮かびます)
どのプロセッサ用にコンパイルしますか?
テキサスインスツルメンツの62xxシリーズのDSP向けのコンパイラは、 「プログラム全体の最適化」。 (無効にすることができます)
これは、ローカルだけでなく、コードが再配置される場所です。そのため、実行の順序は予想したものとは異なります。
CとC ++は、実際にはメモリモデル(
それらを知らない人にとって、62xxファミリはクロックサイクルDSPあたり8命令です。 750Mhzでは、6e + 9命令でピークに達します。とにかくいくつかの時間。並列実行を行いますが、命令の順序付けは、Intel x86のようなCPUではなくコンパイラで行われます。
PICとRabbitの組み込みボードは、特にきちんと確認しない限り、スタックを しません。
それはコンパイラ固有のものであり、そのようにしたい場合は逆にする独自のコンパイラを作成できます。
まともなコンパイラーは、可能であれば、ローカル変数をレジスターに入れます。変数は、レジスターに過度のプレッシャーがある場合(十分なスペースがない場合)、または変数のアドレスが取得された場合にのみ、スタックに配置する必要があります。
私が知る限り、C / C ++の場合、スタック上の特定の位置またはアライメントに変数を配置する必要があるということはありません。コンパイラーは、パフォーマンスに最適な場所、および/またはコンパイラー作成者にとって便利な場所にそれらを配置します。
AFAIKでは、CまたはC ++の定義には、コンパイラがスタック上のローカル変数を順序付ける方法を指定するものは何もありません。コンパイラの次のバージョンでは異なる方法で実行される可能性があるため、この場合にコンパイラが実行できることを信頼するのは悪い考えだと思います。数バイトのスタックを節約するためにローカル変数を順序付けるために時間と労力を費やす場合、それらの数バイトはシステムの機能にとって非常に重要です。
C標準が必要とするもの、または必要としないものについて、何も考えずに推測する必要はありません。最近のドラフトは、 ANSI / ISOワーキンググループ。
これはあなたの質問には答えませんが、関連する問題についての私の2セントです...
スタックスペースの最適化の問題はありませんでしたが、スタック上のdouble変数の不整合の問題がありました。関数は他の関数から呼び出すことができ、スタックポインター値には非境界整列値が含まれる場合があります。そこで、以下のアイデアを思いつきました。これは元のコードではなく、私が書いたばかりです...
#pragma pack(push, 16)
typedef struct _S_speedy_struct{
double fval[4];
int64 lval[4];
int32 ival[8];
}S_speedy_struct;
#pragma pack(pop)
int function(...)
{
int i, t, rv;
S_speedy_struct *ptr;
char buff[112]; // sizeof(struct) + alignment
// ugly , I know , but it works...
t = (int)buff;
t += 15; // alignment - 1
t &= -16; // alignment
ptr = (S_speedy_struct *)t;
// speedy code goes on...
}