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(),但最终,只会将一个值复制到result结构中。可以想象,随着这个函数的结构更大、案例更多、递归调用更多,可用的堆栈空间被迅速消耗掉。

如果返回类型是POD,如上面不带/DPKTS指令编译时,每个case直接拷贝到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++ 编译器是这三个编译器中唯一为开关中的每种情况保留空间的编译器。

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

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 回答