Floating point number conversion horror, is there a way out?

后端 未结 1 463
日久生厌
日久生厌 2021-01-03 08:14

Background

Recently my colleague add some new tests to our test project. One of them has not passed on or continuous integration system. Since we ha

1条回答
  •  挽巷
    挽巷 (楼主)
    2021-01-03 08:22

    The problem is most likely because something else in your code is changing the floating point rounding mode. Have a look at this program:

    {$APPTYPE CONSOLE}
    
    {$R *.res}
    
    uses
      SysUtils, Math;
    
    const
      CStrGPS = 'N5145.37936E01511.8029';
    var
      LLatitude, LLongitude: Integer;
      LLong: Double;
      LStrLong, LTmpStr: String;
      LFS: TFormatSettings;
    
    begin
      FillChar(LFS, SizeOf(LFS), 0);
      LFS.DecimalSeparator := '.';
    
      LStrLong := Copy(CStrGPS, Pos('E', CStrGPS)+1, 10);
      LTmpStr := Copy(LStrLong,1,3);
      LLong := StrToFloatDef( LTmpStr, 0, LFS );
      LTmpStr := Copy(LStrLong,4,10);
      LLong := LLong + StrToFloatDef( LTmpStr, 0, LFS)*1/60;
    
      Writeln(FloatToStr(LLong));
      Writeln(FloatToStr(LLong*100000));
    
      SetRoundMode(rmNearest);
      LLongitude := Round(LLong * 100000);
      Writeln(LLongitude);
    
      SetRoundMode(rmDown);
      LLongitude := Round(LLong * 100000);
      Writeln(LLongitude);
    
      SetRoundMode(rmUp);
      LLongitude := Round(LLong * 100000);
      Writeln(LLongitude);
    
      SetRoundMode(rmTruncate);
      LLongitude := Round(LLong * 100000);
      Writeln(LLongitude);
    
      Readln;
    end.
    

    The output is:

    15.196715
    1519671.5
    1519671
    1519671
    1519672
    1519671
    

    Clearly your particular calculation depends on the floating point rounding mode as well as the actual input value and the code. Indeed the documentation does make this point:

    Note: The behavior of Round can be affected by the Set8087CW procedure or System.Math.SetRoundMode function.

    So you need to first of all find whatever else in your program is modifying the floating point control word. And then you must make sure that you set it back to the desired value whenever that mis-behaving code executes.


    Congratulations on debugging this further. In fact it is actually the multiplication

    LLong*100000
    

    which is influenced by the precision control.

    To see that this is so, look at this program:

    {$APPTYPE CONSOLE}
    var
      d: Double;
      e1, e2: Extended;
    begin
      d := 15.196715;
      Set8087CW($1272);
      e1 := d * 100000;
      Set8087CW($1372);
      e2 := d * 100000;
      Writeln(e1=e2);
      Readln;
    end.
    

    Output

    FALSE
    

    So, precision control influences the results of the multiplication, at least in the 80 bit registers of the 8087 unit.

    The compiler doesn't store the result of that multiplication to a variable and it remains in the FPU, so this difference flows on to the Round.

    Project1.dpr.9: Writeln(Round(LLong*100000));
    004060E8 DD05A0AB4000     fld qword ptr [$0040aba0]
    004060EE D80D84614000     fmul dword ptr [$00406184]
    004060F4 E8BBCDFFFF       call @ROUND
    004060F9 52               push edx
    004060FA 50               push eax
    004060FB A1107A4000       mov eax,[$00407a10]
    00406100 E827F0FFFF       call @Write0Int64
    00406105 E87ADEFFFF       call @WriteLn
    0040610A E851CCFFFF       call @_IOTest
    

    Notice how the result of the multiplication is left in ST(0) because that's exactly where Round expects its parameter.

    In fact, if you pull the multiplication into a separate statement, and assign it to a variable, then the behaviour becomes consistent again:

    tmp := LLong*100000;
    LLongitude := Round(tmp);
    

    The above code produces the same output for both $1272 and $1372.

    There basic issue remains though. You have lost control of the floating point control state. To deal with this you'll need to keep control of your FP control state. Whenever you call into a library that may modify it, store it away before calling, and then restore when the call returns. If you want to have anything like repeatable, reliable and robust floating point code, this sort of game is, unfortunately, inevitable.

    Here is my code to do that:

    type
      TFPControlState = record
        _8087CW: Word;
        MXCSR: UInt32;
      end;
    
    function GetFPControlState: TFPControlState;
    begin
      Result._8087CW := Get8087CW;
      Result.MXCSR := GetMXCSR;
    end;
    
    procedure RestoreFPControlState(const State: TFPControlState);
    begin
      Set8087CW(State._8087CW);
      SetMXCSR(State.MXCSR);
    end;
    
    var
      FPControlState: TFPControlState;
    ....
    FPControlState := GetFPControlState;
    try
      // call into external library that changes FP control state
    finally
      RestoreFPControlState(FPControlState);
    end;
    

    Note that this code handles both floating point units and so is ready for 64-bit which uses the SSE unit rather than the 8087 unit.


    For what it is worth, here is my SSCCE:

    {$APPTYPE CONSOLE}
    var
      d: Double;
    begin
      d := 15.196715;
      Set8087CW($1272);
      Writeln(Round(d * 100000));
      Set8087CW($1372);
      Writeln(Round(d * 100000));
      Readln;
    end.
    

    Output

    1519672
    1519671
    

    0 讨论(0)
提交回复
热议问题