Visual C++ 2012(x86)可能存在编译器错误?

29

我目前在使用VC++ 11(CTP更新1版)编译x86目标时,遇到了随机浮点错误。请参见下面的简短示例“test.cpp”,并使用以下方式进行编译:

cl /GL /O2 /EHsc test.cpp /link /MACHINE:X86

输出应该是10 == 10,但当启用/GL(整个程序优化)时,它会产生10 == 0。问题似乎是get_scaling_factor()将结果推送到浮点堆栈上,但调用函数期望在SSE寄存器XMM0中得到结果。 问题:我是否遗漏了一些明显的东西,还是这真的是一个bug?当然,测试程序没有意义,因为它只是一个简化的测试案例。 test.cpp:
#include <iostream>

template <typename T>
inline T get_scaling_factor(int units)
{
    switch (units)
    {
    case 0: return 1;  
    case 1: return 10;  
    case 2: return 100;  
    case 3: return 1000;  
    case 4: return 10000;  
    case 5: return 100000;  
    case 6: return 1000000;  
    case 7: return 10000000;  
    case 8: return 100000000;  
    case 9: return 1000000000; 
    default: return 1;
    }
}

template <int targetUnits, typename T>
inline T scale(T value, int sourceUnits)
{
    return value   * get_scaling_factor<T>(sourceUnits) 
                   / get_scaling_factor<T>(targetUnits);
}

__declspec(noinline)
double scale(double value, int units) 
{
    return scale<9>(value, units);
}

int main()
{
    std::cout << "10 = " << scale(1e9, 1) << std::endl;
}

更新

微软已确认问题。即使是像这样简单的代码也会受到影响:

#include <stdio.h>
double test(int a)
{
    switch (a)
    {
    case 0: return 1.0;
    case 1: return 10.0;
    case 2: return 100.0;
    case 3: return 1000.0;
    case 4: return 10000.0;
    case 5: return 100000.0;
    case 6: return 1000000.0;
    case 7: return 10000000.0;
    case 8: return 100000000.0;
    case 9: return 1000000000.0;
    default: return 1.0;
    }
}

void main()
{
    int nine = 9;
    double x = test(nine);
    x /= test(7);
    int val = (int)x;
    if (val == 100)
        printf("pass");
    else 
        printf("fail, val is %d", val);
}

将字面常量转换为类型 T,你会使用 static_cast 吗? - Steve-o
@Steve-o:不行——即使你将所有文字都包装在T(...)中,它也会失败。而且这也不必要,编译器会将它们提升到正确的类型(如果可以的话)。请注意,即使在最高警告级别下,该代码也可以编译而不产生警告。 - Daniel Gehriger
2个回答

23

是的,这绝对是代码优化器的错误,并且我没有遇到任何问题就能复制它。 通常与内联相关的是优化器错误,但在这里不是这种情况。 这个错误是由VS2012中大量的代码生成更改引入的,该更改支持新的自动向量化功能。

简而言之,get_scaling_factor()函数将在FPU堆栈上返回结果。 代码生成器正确地发出指令以从堆栈检索它并将其存储在XMM寄存器中。 但是,优化器不适当地完全删除了该代码,好像它认为函数结果已经存储在XMM0中。

目前很难找到解决方法,为double专门化模板函数没有效果。 使用#pragma optimize禁用优化可行:

#pragma optimize("", off)
__declspec(noinline)
double scale(double value, int units) 
{
    return scale<9>(value, units);
}
#pragma optimize("", on)

您的复现代码非常好,Microsoft不会有任何问题来修复这个bug。 您可以在connect.microsoft.com上提交反馈报告,只需链接到此问题即可。或者如果您很着急,可以联系Microsoft支持,尽管我想象他们会给您相同的解决方法,以使您适用于服务包。


更新:在VS2013中已经修复。


1
Hans,感谢您提供这个有用的分析。我采纳了您的建议并提交了一个错误报告 - Daniel Gehriger
1
关于你的解决方法:实际上只需要重写 get_scaling_factor() 并引入一个变量 T result;,在 switch 语句中将正确的返回值分配给它,然后再返回即可。编译器会生成正确的结果。但是,由于我不知道到底是什么触发了这个 bug,所以我必须假设它随时可能出现。这基本上意味着 LTCG 不能用于生产代码 - Daniel Gehriger
确认。只需添加一个本地变量并初始化即可使函数在优化代码中突然使用xmm0而不是FPU作为返回值。看起来真正的bug是优化器失去了对函数使用FPU或SSE的追踪。 - Hans Passant

1

/GL 是有意忽略默认的调用约定。使用 LTCG,编译器/链接器了解整个调用图,因此可以匹配调用者和被调用者。在这种情况下,使用 SSE 寄存器并不奇怪。

我不太确定你所说的 "get_scaling_factor() 将结果推送到浮点堆栈" 是什么意思。你是指编译器无法内联它吗?我期望编译器会这样做,因为调用图只有一个调用者。(我们知道 `get_scaling_factor(targetUnits)` 已经被内联了,否则就会导致除以零)

如果编译器确实无法内联 get_scaling_factor(),那么你实际上发现了两个错误:一个是内联失败,另一个是自定义调用约定失败。


感谢您的回答。编译器半内联 get_scaling_factor():它将 switch 语句的默认分支内联,但不包括剩余部分。文本字符串通过浮点栈(使用 fld)传递,但调用函数希望它们在 xmm0 中。 - Daniel Gehriger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接