Eric Fleegal's WebLog

. . . .

Method for getting /Op like consistency in MS C++ 14.0

(From Microsoft Visual C++ Floating-Point Optimization)

 

Many C++ compilers offer a "consistency" floating-point model (through a /Op or /fltconsistency switch) that enables a developer to create programs compliant with strict floating-point semantics. When engaged, this model prevents the compiler from using most optimizations on floating-point computations while allowing those optimizations for non-floating-point code. The consistency model, however, has a dark-side. In order to return predictable results on different FPU architectures, nearly all implementations of /Op round intermediate expressions to the user specified precision; for example, consider the following expression:

  float a, b, c, d, e; 
  . . .
  a = b*c + d*e;

In order to produce consistent and repeatable results under /Op, this expression gets evaluated as if it were implemented as follows:

  float x = b*c; 
  float y = d*e;
  a = x+y;

The final result now suffers from single-precision rounding errors at each step in evaluating the expression. Although this interpretation doesn't strictly break any C++ semantics rules, it's almost never the best way to evaluate floating-point expressions. It is generally more desirable to compute the intermediate results in as high as precision as is practical. For instance, it would be better to compute the expression a=b*c+d*e in a higher precision as in,

 

In short, the old /Op model trades away accuracy for consistency across platforms.  For nearly all numerical programs, accuracy is preferable. 

This is precisely (no pun intended) the reason VC abandoned the consistency model altogether (and I fully expect other compiler makers to follow suite)

 

There are however rare cases when consistency across platforms may be desired.  In obviating the /Op model, Microsoft C++ 14.0 (in VS8.0) no longer provides a simple command line switch to enable cross-platform floating-point consistency.  To get consistency across platforms, programmers will need to modify their source code.  In the case from the whitepaper (above):

  float a, b, c, d, e; 
  . . .
  a = b*c + d*e;

the results of the expression b*c + d*e are dependant on the intermediate precision (i.e. the register precision) of the target platform.  To enforce consistency across all platforms, users will need to introduce explicit narrowing operations at each point in the computation (setting _controlfp won’t achieve the same results for reasons outlined in [1])

  float a, b, c, d, e; 
  . . .
  a = float(b*c) + float(d*e);

Of course this is rather inconvenient to say the least.  A more convenient method is to introduce a new “wrapper” class that will implicitly enforce consistency semantics.  Such a class will enable the code to be rewritten as

  cfloat a, b, c, d, e; 
  . . .
  a = b*c + d*e;

which is clearly a simpler and more elegant solution (I named it “cfloat” for “consistent float”).  By overloading the arithmetic operators for the wrapper class cfloat, we can make it behave as if it were the built in floating-point type. 

 

We begin by introducing a new type that wraps the floating point types (I’ll only show single precision float here, however the same method would apply to a wrapper class for double or long double).

 

 class cfloat

 {

 public:

    float value;

    cfloat() {}

    cfloat(const cfloat& v) : value(v.value) {}

    cfloat(float v) : value(v) {}

    . . .

 }

 

 Similarly for types double and long doulble

 

Naturally, we’ll need the assignment operators:

 

 class cfloat

 {   

    . . .

    cfloat& operator = (const cfloat& v)

    {

        value = v;

        return *this;

    }

 

    cfloat& operator = (float v)

    {

        value = v;

        return *this;

    }

 

    Similarly for each operator +=, -=, *=, and /=

  

 }

 

We’ll also want to introduce explicit operators for narrowing values from double and long double precisions (note that long-double isn’t strictly necessary)

 

 class cfloat

 {   

    . . .

 

    explicit cfloat(const cdouble& v) : value((float)v.value) {}

 

    explicit cfloat(double v) : value((float) v) {}

 

    Similarly for each long double

  

 }

 

Then, introduce versions of each arithmetic operation

 

 inline cfloat operator + (const cfloat& a, const cfloat& b)

 {

    return a.value + b.value;   

 }

 

 inline cfloat operator + (float a, const cfloat& b)

 {

    return a + b.value;   

 }

 

 inline cfloat operator + (const cfloat& a, float b)

 {

    return a.value + b;   

 }

 

 Similarly for -, *, and /

 

Strictly speaking the 2nd and 3rd variations of the operators aren’t necessary, but they do make debugging a bit easier.

 

When the methods of the cfloat class are nicely inlined, the runtime performance of this class should be no worse than under the old /Op model. 

 

 

Published Wednesday, July 28, 2004 4:53 PM by ericflee
Anonymous comments are disabled

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker