4

レガシー コードベースのツールチェーンを更新する一環として、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 つだけです。

テスト コードのどの部分が変更される可能性があるかがわからない場合、リファクタリング オプションを提案するのが難しいことはわかっています。そのため、最初の質問は、テスト ケースでコンパイラがこのように動作する理由です。それを理解できれば、最適なリファクタリング/プラグマ/コマンドライン オプションを選択して修正できることを願っています。

4

2 に答える 2

2

なぜだけではないのですか

void findresult(int key, RETURN_TYPE* result)
{
   if (key >= 1 && key <= 11)
     *result = pickone(4+key);
   else
     *result = error;
}

これが小さな変更と見なされると仮定すると、特に組み込みコンパイラに関連する、スコープに関する古い質問を思い出しました。各ケースを中かっこで囲んで一時スコープを明示的に制限すると、オプティマイザーはよりうまく機能しますか?

switch(key)
{
   case 1   : { *result = pickone(5 ); break; }

別のスコープ変更オプション:

void findresult(int key, RETURN_TYPE* result)
{
    RETURN_TYPE tmp;
    switch(key)
    {
      case 1   : tmp = pickone(5 ); break;
      ...
    }
    *result = tmp;
}

どの入力がこの不運なオプティマイザからの適切な応答を誘導するかを推測しようとしているだけなので、これはすべて少し複雑です。

于 2013-01-04T18:11:21.700 に答える
0

変更が関数の外に「漏れ」ない限り、その関数の書き換えは許可されていると想定します。また、(コメントで述べたように)実際には、呼び出す個別の関数がいくつかあると想定しています(ただし、それらはすべて同じタイプの入力を受け取り、同じ結果タイプを返します)。

そのような場合、私はおそらく関数を次のようなものに変更します。

RETURN_TYPE func1(int) { /* ... */ }
RETURN_TYPE func2(int) { /* ... */ }
// ...

void findresult(int key, RETURN_TYPE *result) { 
    typedef RETURN_TYPE (*f)(int);

    f funcs[] = (func1, func2, func3, func4, func5, /* ... */ };

    if (in_range(key))
        *result = funcs[key](key+4);
    else
        *result = error;
}
于 2013-01-04T18:23:55.853 に答える