4

I have release and debug codes that can give dramatically different results. Since I can't debug release code I have been printing value to a file to compare the two modes. I have localized my problem to a few lines of code.

var yFloat = topArcDetails.ArcPoints.GetInterpolatedYAt(point.X);
var tvdOnTopHorizon = (double) yFloat;
var yPointDouble = (double) point.Y;
deltaTvdTop = yPointDouble - tvdOnTopHorizon;
if (point.X == 2102.0 && point.Y > 1573.0 && point.Y < 1574.0)
{
    var message = String.Format("MD, TVD,         tvdOnTopHorizon, deltaTvdTop = {0}  {1}          {2}  {3}", point.X, point.Y, tvdOnTopHorizon, deltaTvdTop);
    //var message = String.Format("MD, TVD, yFloat, tvdOnTopHorizon, deltaTvdTop = {0}  {1}  {2}  {3}  {4}", point.X, point.Y, yFloat, tvdOnTopHorizon, deltaTvdTop);
    var oldOut = Console.Out;
    using (TextWriter w = File.AppendText("debugOutput.txt"))
    {
        Console.SetOut(w);
        Console.WriteLine(message);
    }
    Console.SetOut(oldOut);
}

Note that I have two choices of the message I can print to file. One message contains the value of yFloat, the other does not.

If I run in release mode without debugging the first message will give:

MD, TVD,         tvdOnTopHorizon, deltaTvdTop = 2102  1573.135            1554.6184437573   18.5164439380169

The second message will give:

MD, TVD, yFloat, tvdOnTopHorizon, deltaTvdTop = 2102  1573.135  1554.618  1554.61840820313  18.5164794921875

Notice that just printing the value of yFloat causes small changes in the last two numbers. In debug mode I see no difference caused by the print statement the two results are:

MD, TVD, yFloat, tvdOnTopHorizon, deltaTvdTop = 2102  1573.135  1554.618  1554.61840820313  18.5164794921875
MD, TVD,         tvdOnTopHorizon, deltaTvdTop = 2102  1573.135            1554.61840820313  18.5164794921875

The debug result agree with the release results where yFloat is printed. I know that the core issue is the fact that the code mixes floating point and doubles, which was someone else's bad idea. Although printing the value of yFloat fixes the problem, I don't think that is very practical. It does make me think that there must be something I can do to get the release version to agree with the debug version.

O. Jones
  • 103,626
  • 17
  • 118
  • 172
spainchaud
  • 365
  • 3
  • 12
  • I believe your issue is explained in http://stackoverflow.com/questions/6683059/is-floating-point-math-consistent-in-c-can-it-be. However, I have gold badge dupe hammer rights so I'm not voting to close as a duplicate because I'm not 100% sure. My guess is that in debug mode computations are stored in 32 bit and 64 memory locations. In release mode computations are stored in 80 registers on the FPU leading to different results. – Martin Liversage Jan 25 '16 at 22:28
  • I will check it out. I will note that I have eliminated all the printing and just added var str = String.Format("{0}", yFloat); after I calculate yFloat. The program now works perfectly, but somehow it does not feel like a solution. – spainchaud Jan 25 '16 at 22:33
  • Martin, I looked at that post and I am not seeing much that makes much sense. – spainchaud Jan 25 '16 at 22:44
  • What do you get if you do `var yFloat = (float) topArcDetails.ArcPoints.GetInterpolatedYAt(point.X);` (i.e. forcing the cast) without the string conversion? – Matthew Strawbridge Jan 25 '16 at 23:10
  • I did not try the explicit cast because the method GetInterpolatedYAt returns a float. I can try it when I get to work tomorrow. – spainchaud Jan 26 '16 at 04:57
  • @spainchaud MatthewStrawbridge's point is that at high optimization, the compiler may have inlined `GetInterpolatedYAt`, and then §4.1.6 kicks in, where it says the compiler is allowed to use higher precision than that of the nominal result. In your case, it uses `double` or possibly even the doesn't-have-a-name-in-C# 80-bit extended double. – Raymond Chen Jan 26 '16 at 16:01
  • Thanks Matthew, the redundant cast to float is not redundant after all. At least in this case. I will have to add a comment so no one will mistakenly remove the cast. – spainchaud Jan 26 '16 at 17:14
  • @RaymondChen, thanks for the explanation. Did you intend to add a link when you referenced §4.1.6. I would like to get more information on what is going on. – spainchaud Jan 26 '16 at 17:16
  • The link is in Martin's comment. – Raymond Chen Jan 26 '16 at 18:01

1 Answers1

2

The C# language specification (you should have a copy in a folder like C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC#\Specifications\1033 or you can download it) says this in Section 4.1.6 Floating point types:

Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an “extended” or “long double” floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type. Only at excessive cost in performance can such hardware architectures be made to perform floating-point operations with less precision, and rather than require an implementation to forfeit both performance and precision, C# allows a higher precision type to be used for all floating-point operations. Other than delivering more precise results, this rarely has any measurable effects. However, in expressions of the form x * y / z, where the multiplication produces a result that is outside the double range, but the subsequent division brings the temporary result back into the double range, the fact that the expression is evaluated in a higher range format may cause a finite result to be produced instead of an infinity.

(The key point is the sentence I've put in bold.)

Essentially this means the C# compiler is free to optimise the code however it likes as long as the accuracy gets better not worse.

In your case, although topArcDetails.ArcPoints.GetInterpolatedYAt(point.X) returns a float, the result is used for calculating a double. Apparently the compiler is inlining the code as an optimisation when building the release version, bypassing the conversion to float. (You might also find differences in the output when compiling in x86 release vs. x64 release modes.)

You can explicitly cast the value, even though this looks redundant,

var yFloat = (float) topArcDetails.ArcPoints.GetInterpolatedYAt(point.X);

which will force the compiler to convert the result into a float before using it.

So in conclusion you have to choose between accuracy and repeatability. If you care most about getting the most accurate result (and perhaps better performance), allow the compiler to do its optimisations. If you care more about getting identical results across platforms then you'll need to add extra casts by hand to force intermediate calculations into a fixed number of bits.

Matthew Strawbridge
  • 19,940
  • 10
  • 72
  • 93
  • I guess these kinds of gotchas sneak up on you when one is not aware of the internals of .NET and the C# compiler. I have come across similar problems with precision and general prefer using decimal whenever I can manage it and it makes sense to do so (strange that Silverlight stripped out a lot of BCL functions that operated on decimal). – Eniola Jan 29 '16 at 20:40
  • 1
    @Eniola Good point about `decimal`. There are some gotchas with all number representations, though; for example `Decimal.MinValue + 0.1M == Decimal.MinValue` is true. But with the binary floating-point types it's generally best to always treat the values as (close) approximations, which is often all you need. – Matthew Strawbridge Jan 29 '16 at 21:14
  • Now that is a strange one. Either I have not run into it, or I did not take note. Any word from the BCL team as to why that is the case? Is it part of the standard or an error in implementation? Does one get the same result on Mono or the new .NET Core? – Eniola Feb 04 '16 at 09:59
  • @Eniola I think it's just that there are only a limited number of bits to use, so you always lose precision at the extremes. – Matthew Strawbridge Feb 04 '16 at 12:23