レガシー コードベースのツールチェーンを更新する一環として、Borland C++ 5.02 コンパイラから Microsoft コンパイラ (VS2008 以降) に移行したいと考えています。これは、スタック アドレス空間が事前定義され、かなり制限されている組み込み環境です。大きな switch ステートメントを含む関数があることがわかりました。これにより、MS コンパイラでは、Borland のコンパイラよりもはるかに大きなスタック割り当てが発生し、実際にはスタック オーバーフローが発生します。
コードの形式は次のようなものです。
#ifdef PKTS
#define RETURN_TYPE SPacket
typedef struct
{
int a;
int b;
int c;
int d;
int e;
int f;
} SPacket;
SPacket error = {0,0,0,0,0,0};
#else
#define RETURN_TYPE int
int error = 0;
#endif
extern RETURN_TYPE pickone(int key);
void findresult(int key, RETURN_TYPE* result)
{
switch(key)
{
case 1 : *result = pickone(5 ); break;
case 2 : *result = pickone(6 ); break;
case 3 : *result = pickone(7 ); break;
case 4 : *result = pickone(8 ); break;
case 5 : *result = pickone(9 ); break;
case 6 : *result = pickone(10); break;
case 7 : *result = pickone(11); break;
case 8 : *result = pickone(12); break;
case 9 : *result = pickone(13); break;
case 10 : *result = pickone(14); break;
case 11 : *result = pickone(15); break;
default : *result = error; break;
}
}
でコンパイルするとcl /O2 /FAs /c /DPKTS stack_alloc.cpp
、リスト ファイルの一部は次のようになります。
_TEXT SEGMENT
$T2592 = -264 ; size = 24
$T2582 = -240 ; size = 24
$T2594 = -216 ; size = 24
$T2586 = -192 ; size = 24
$T2596 = -168 ; size = 24
$T2590 = -144 ; size = 24
$T2598 = -120 ; size = 24
$T2588 = -96 ; size = 24
$T2600 = -72 ; size = 24
$T2584 = -48 ; size = 24
$T2602 = -24 ; size = 24
_key$ = 8 ; size = 4
_result$ = 12 ; size = 4
?findresult@@YAXHPAUSPacket@@@Z PROC ; findresult, COMDAT
; 27 : switch(key)
mov eax, DWORD PTR _key$[esp-4]
dec eax
sub esp, 264 ; 00000108H
...
$LN11@findresult:
; 30 : case 2 : *result = pickone(6 ); break;
push 6
lea ecx, DWORD PTR $T2584[esp+268]
push ecx
jmp SHORT $LN17@findresult
$LN10@findresult:
; 31 : case 3 : *result = pickone(7 ); break;
push 7
lea ecx, DWORD PTR $T2586[esp+268]
push ecx
jmp SHORT $LN17@findresult
$LN17@findresult:
call ?pickone@@YA?AUSPacket@@H@Z ; pickone
mov edx, DWORD PTR [eax]
mov ecx, DWORD PTR _result$[esp+268]
mov DWORD PTR [ecx], edx
mov edx, DWORD PTR [eax+4]
mov DWORD PTR [ecx+4], edx
mov edx, DWORD PTR [eax+8]
mov DWORD PTR [ecx+8], edx
mov edx, DWORD PTR [eax+12]
mov DWORD PTR [ecx+12], edx
mov edx, DWORD PTR [eax+16]
mov DWORD PTR [ecx+16], edx
mov eax, DWORD PTR [eax+20]
add esp, 8
mov DWORD PTR [ecx+20], eax
; 41 : }
; 42 : }
add esp, 264 ; 00000108H
ret 0
割り当てられたスタック領域には、 から返された構造体を一時的に格納するためのケースごとの専用の場所が含まれpickone()
ていますが、最終的には 1 つの値のみがresult
構造体にコピーされます。ご想像のとおり、構造体が大きく、ケースが多く、この関数で再帰呼び出しが行われると、使用可能なスタック領域が急速に消費されます。
上記が/DPKTS
ディレクティブなしでコンパイルされたときのように、戻り値の型が POD の場合、各ケースは に直接コピーされresult
、スタックの使用がより効率的になります。
$LN10@findresult:
; 31 : case 3 : *result = pickone(7 ); break;
push 7
call ?pickone@@YAHH@Z ; pickone
mov ecx, DWORD PTR _result$[esp]
add esp, 4
mov DWORD PTR [ecx], eax
; 41 : }
; 42 : }
ret 0
コンパイラがこのアプローチを採用する理由と、そうしないように説得する方法があるかどうかを誰かが説明できますか? コードを再構築する自由は限られているため、プラグマなどのソリューションがより望ましいソリューションです。これまでのところ、違いを生む最適化、デバッグなどの引数の組み合わせは見つかりませんでした。
ありがとうございました!
編集
findresult()
の戻り値にスペースを割り当てる必要があることを理解していますpickone()
。私が理解していないのは、コンパイラがスイッチの可能なケースごとに追加のスペースを割り当てる理由です。一時的な 1 つのスペースで十分なようです。実際、これは gcc が同じコードを処理する方法です。一方、Borland は RVO を使用しているようで、ポインターをずっと下に渡し、一時的な使用を避けています。MS C++ コンパイラは、スイッチの各ケースにスペースを予約する 3 つのうちの 1 つだけです。
テスト コードのどの部分が変更される可能性があるかがわからない場合、リファクタリング オプションを提案するのが難しいことはわかっています。そのため、最初の質問は、テスト ケースでコンパイラがこのように動作する理由です。それを理解できれば、最適なリファクタリング/プラグマ/コマンドライン オプションを選択して修正できることを願っています。