user1713712
user1713712

Reputation: 39

Float operation optimization leads to rare and strange behaviour with Visual C++

I am doing several floating point operations in a routine written in C++ and compiled with Visual C++ 2008. I also have optimizations activated (/O2).

The code in C++ looks roughly like this:

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 is a singleton that contains a structure of parameters used for the calculation. This is meant to be referenced to by the configParams reference.

With optimization activated, on some rare occasions I get calculation errors with completely incorrect results.

Looking at the dissassembly I see this:

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;
    ...
}

First of all, we see that it does not call Parameters(). This is understandable since the parameters structure lies in the class, 8 bytes in (after two other floats). So after the call, eax has the address to the ConfigReader CLASS (not the ConfigurationParams structure).

Then it attempts to load a float. This is where the problem occurs. For some reason the offset for the load operation is incorrect for an ebx that is pointing to the ConfigReader class. It should add 8 first for the offset to be correct.

Is it possible that the compiler assumes that the fld operation will take longer than the add operation and that somehow ebx will have 8 added to it before the float is loaded from memory? Can this work? Could our occasional problem stem from an interrupt happening right at this point and causing ebx to not have the offset of 8 by the time the float is loaded?

I would expect that the only way for this to be correct is for the add operation to be before fld. It is hard to understand that this works at all...

Is there any way to turn off this kind of rearrangement optimization?

Edit: ConfigReader looks like this

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;

}

Upvotes: 3

Views: 235

Answers (2)

Alexey Frunze
Alexey Frunze

Reputation: 62106

I can hardly believe there's a bug in the variable addresses generated by the compiler. It's not impossible, but highly uncommon. And this has to be ruled out first, but it doesn't look like we are provided with all the relevant code to make a judgment on that.

What is most likely to be happening, however, is that the compiler is doing one or two of the following:

  1. it reorders the floating point operations when doing aggressive floating point optimizations
  2. it makes some computations happen with extended precision

The consequences of (1) are such that, although the optimizations are correct from the mathematical standpoint, math axioms cease to work in calculations with limited precision, which is why arbitrary (at compiler's whim) reordering causes the results to differ from what you may be expecting at all or relative to the results in the unoptimized version of the same code.

The hard truth is that in most computers floating-point arithmetic works like this:

(a+b)+c ≠ a+(b+c)
(a*b)*c ≠ a*(b*c)
(a+b)*c ≠ (a*c)+(b*c)
... and so on (see Knuth's TAOCP or the already mentioned Microsoft Visual C++ Floating-Point Optimization).

So, reordering of floating-point operations is generally bad for consistency.

The consequences of (2), which is allowed per the C standard, are such that you can get intermediate results with different precision in code that's optimized differently. The final results should be expected to be different as well.

What I'd do is first try /fp:strict, if /fp:precise is in use (it's the default). Definitely, you don't want /fp:fast, if you need consistency.

Upvotes: 0

Jim Buck
Jim Buck

Reputation: 20724

Actually, the fld looks correct to me. It's the fmul that looks like it's taking the wrong value. The code generated is doing:

float m1 = configParams.p1 * configParams.f14;

Given that you are compiling this with optimizations, and you didn't post the complete code, are you sure it's just not doing the stuff out of order relative to your code? Or, are you sure the struct definition is the correct one? It seems you've anonymized and abbreviated this, thus making the posted code different from what you are actually seeing.

Upvotes: 1

Related Questions