浮点运算优化导致在Visual C++中出现罕见和奇怪的行为

3

我正在使用Visual C++ 2008编译的程序中执行一些浮点运算。此外,我还启用了优化(/O2)。

C++代码大致如下:

int Calculate( CalculationParams &params )
{
    const ConfigurationParams& configParams = ConfigReader::Instance().Parameters();

    float m1 = configParams.p1 * configParams.p2;
    float m2 = configParams.p3 * configParams.p4;
    float m3 = configParams.p5 * configParams.p6;

    ....
}

ConfigReader是一个单例对象,包含了计算所需的参数结构。这个对象可以通过configParams引用来进行访问。

当优化被激活时,偶尔会出现错误的计算结果。

查看反汇编代码,我发现了以下信息:

int Calculate( CalculationParams &params )
{
    ...
    const ConfigurationParams& configParams = ConfigReader::Instance().Parameters();
        call ConfigReader::Instance()
        move ebx, eax

    float m1 = configParams.p1 * configParams.p2;
        fld dword ptr[ebx + 0D4h]
        add ebx, 8
        fmul dword ptr [ebx + 0ECh]
    float m2 = configParams.p3 * configParams.p2;
    float m3 = configParams.p4 * configParams.p2;
    ...
}

首先,我们可以看到它没有调用Parameters()。这是可以理解的,因为参数结构位于类中,8个字节之后(在两个其他浮点数之后)。因此,在调用之后,eax具有ConfigReader CLASS的地址(而不是ConfigurationParams结构的地址)。
然后它尝试加载一个浮点数。这就是问题出现的地方。由于ebx指向ConfigReader类时,加载操作的偏移量似乎不正确,所以问题就出现了。它应该先添加8才能使偏移量正确。
编译器是否认为fld操作比add操作花费更长的时间,并且在从内存加载浮点数之前,ebx将自动添加8?这样行得通吗?我们偶尔遇到问题是否源于在此时发生中断,导致ebx在加载浮点数时没有偏移量为8?
我期望唯一正确的方法是在fld之前进行add操作。很难理解这样甚至会起作用...
有没有办法关闭这种重新排列优化呢?
编辑: ConfigReader如下:
class ConfigReader
{
public:
    static ConfigReader& Instance();

    const ConfigurationParams& Parameters() const { return myParameters; }

private:
    ConfigReader();

    float internalParam1;
    float internalParam2;

    ConfigurationParams myParameters;
}



struct ConfigurationParams
{
    char s1[10];
    char s2[50];
    int i1;
    int i2;
    int i3;
    int i4;
    int i5;
    int i6;
    int i7;
    int i8;
    int i9;
    int i10;
    int i11;
    int i12;
    int i13;
    int i14;
    int i15;
    int i16;
    int i17;
    int i18;
    int i19;
    int i20;
    int i21;
    int i22;
    int i23;

    float f1;
    float f2;

    int i25;
    int i26;

    int i27;
    int i28;
    int i29;

    int i30;
    int i31;

    bool b1;

    float f3;
    float f4;
    float f5;
    float p1;
    float p3;
    float p4;
    float f9;
    float f10;
    float f11;
    float f12;
    float p2;
    float f14;
    float f15;
    float f16;

    int i32;

    int i33;
    int i34;
    int i35;

}


2
你真的需要发布完整的代码,连同你的类定义一起。 - Jim Buck
我已经找到了问题的根源。我使用的实时操作系统在任务切换时没有存储浮点上下文。这意味着其他任务中的浮点运算可能会影响我的任务。由于其他任务并没有做太多事情,所以这个问题非常罕见,但如果我有另一个任务不断地重复相同的计算,那么这个问题就会更频繁地出现。通过使用处理浮点上下文的驱动程序,这个问题得到了解决。 - user1713712
发布所涉及的操作系统可能会有所帮助。 ;) (根据生成的汇编代码,我假设它是主要的三个操作系统之一,即Windows、OSX或Linux,所有这些操作系统都应该能够很好地处理事情。) - Jim Buck
2个回答

1

实际上,fld 对我来说看起来是正确的。而 fmul 看起来似乎取了错误的值。生成的代码执行的是:

float m1 = configParams.p1 * configParams.f14;

鉴于您正在使用优化编译此代码,并且您没有发布完整的代码,您确定它只是按照与您的代码不同的顺序执行操作吗?或者,您确定结构体的定义是正确的吗?看起来您已经将代码匿名并缩写了,因此所发布的代码与您实际看到的不同。


我非常确定,因为ebx+0D4h没有8字节的偏移量是错误的。 - user1713712
p1和p2之间的差异为0x18字节,恰好等于0xEC - 0xD4。ebx的更改绝对不应该在两者之间发生。您是否检查了p1和p2的偏移量? - MSalters
现在看来我找错地方了。重新计算后,ebx+0D4h 看起来是正确的。编译器已经在偏移量中添加了八个字节,然后在后续操作中添加到 ebx 中。错误必须在程序的其他地方。 - user1713712

0

我几乎无法相信编译器生成的变量地址中存在错误。虽然不是不可能,但这种情况非常罕见。首先必须排除这种可能性,但看起来我们没有提供所有相关代码来做出判断。

然而,最有可能发生的情况是编译器正在执行以下一项或两项操作:

  1. 在进行激进的浮点数优化时,重新排序浮点运算
  2. 使用扩展精度进行某些计算

(1)的后果是,尽管从数学角度来看优化是正确的,但在具有有限精度的计算中,数学公理停止起作用,这就是为什么任意(由编译器决定)重新排序会导致结果与您所期望的结果完全不同或相对于同一代码的未优化版本的结果不同。

事实上,在大多数计算机上,浮点数算术运算的工作方式如下:

(a+b)+c ≠ a+(b+c)
(a*b)*c ≠ a*(b*c)
(a+b)*c ≠ (a*c)+(b*c)
...等等(请参阅Knuth的TAOCP或已提到的Microsoft Visual C++浮点优化)。

因此,重新排序浮点运算通常对一致性不利。

根据C标准允许的(2)的后果是,在不同优化的代码中可以获得具有不同精度的中间结果。最终结果也应该是不同的。

我会首先尝试/fp:strict,如果正在使用/fp:precise(这是默认值)。如果需要一致性,绝对不要使用/fp:fast


请看我的回复给Jim。似乎错误不是在这里发生的。 - user1713712
好的,那么就有一些需要审查和调试的地方了。你确定没有使用任何未初始化的变量吗?或者是否依赖于未定义行为如 i + i++a[i++]=i?或者依赖于子表达式求值顺序和未指定函数参数求值顺序吗? - Alexey Frunze

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