如何防止MSVC ++为switch语句过度分配堆栈空间?

时间:2013-01-04 18:06:32

标签: c++ visual-c++ compiler-optimization

作为更新遗留代码库工具链的一部分,我们希望从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()返回的结构,但最后,只有一个值将被复制到result } 结构体。可以想象,对于此函数中较大的结构,更多的情况和递归调用,可用的堆栈空间会被快速消耗。

如果返回类型是POD,就像上面编译时没有/DPKTS指令一样,每个案例都直接复制到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()的返回值分配空间。我不明白的是编译器为交换机中的每种可能情况分配额外空间的原因。似乎一个临时空间就足够了。事实上,这是gcc如何处理相同的代码。另一方面,Borland似乎使用RVO,将指针一直向下传递并避免使用临时。 MS C ++编译器是三个中唯一一个为交换机中的每个案例保留空间的编译器。

我知道当您不知道测试代码的哪些部分可以更改时,很难建议重构选项 - 这就是为什么我的第一个问题为什么编译器的行为方式在测试用例中。我希望如果我能理解,我可以选择最好的重构/ pragma /命令行选项来修复它。

2 个答案:

答案 0 :(得分: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;
}

这有点手持波动,因为我们只是想猜测哪个输入会哄骗这个不幸的优化器做出明智的反应。

答案 1 :(得分: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;
}