浮点数转换恐怖,有出路吗?

2023-12-15

背景

最近,我的同事向我们的测试项目添加了一些新测试。其中之一还没有传递或持续集成系统。由于我们有大约 800 个测试,并且需要一个小时才能运行所有测试,因此我们经常会犯错误,并且只在我们的开发机器上运行我们当前已实现的测试。这种方法有其弱点,因为有时测试在本地通过,但在集成系统上失败。当然,有人可能会说“这不是一个错误,测试应该相互独立!”。

在理想的世界里......当然,但不是在我的世界里。不是在一个有很多单例初始化的世界中initialization部分,Delphi 本身引入的许多全局变量,在后台初始化的 OTL 线程池,连接到控件以用于绘画目的的 DevExpress 方法..以及许多其他我不知道的事情。因此,在最终结果中,一项测试可以改变其他测试的行为。 (这当然是不好的,我很高兴它发生,因为希望我能够修复另一个依赖项)。

我已经在我的机器上启动了整个测试包,并且获得了与集成系统相同的结果。到目前为止一切顺利,现在我开始关闭测试,直到缩小了干扰最近添加的测试的范围。他们没有任何共同点。我进行了更深入的挖掘,并将问题缩小到一行。如果我评论它 - 测试通过,如果没有 - 测试失败。

Problem

我们有这样的代码将文本数据转换为经度坐标(仅包括重要部分):

procedure TTerminalNVCParserTest_Unit.TranslateGPS_ValidGPSString_ReturnsValidCoords;
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;
  LLongitude := Round(LLong * 100000);

  CheckEquals(1519671, LLongitude);
end;

问题是LLongitude有时等于 1519671,有时给出 1519672。并且是否给出 1519672 取决于不同测试中不同方法中其他完全不相关的代码段:

FormXtrMainImport.JvWizard1.SelectNextPage; 

我已经四次检查了 SelectNextPage 方法,它不会触发任何可能改变 FPU 单元工作方式的事件。它不会改变的值RoundingMode它始终设置在 rmNearest 上。

此外,Delphi 不应该在这里使用银行家规则吗? :

LLongitude := Round(LLong * 100000); //LLong * 100000 = 1519671,5

如果使用银行家规则,它应该总是给我 1519672 而不是 1519671。

我猜一定有一些损坏的内存导致了问题以及与SelectNextPage只揭示它。然而,同样的问题出现在三台不同的机器上。

任何人都可以告诉我如何追踪这个问题吗?或者说如何保证始终稳定的转换结果?

对于那些误解我的问题的人

  1. 我已经检查了 RoundingMode 并且之前已经提到过:“我已经四重检查了 SelectNextPage 方法,它不会触发任何可能改变 FPU 单元工作方式的事件。它不会更改 RoundingMode 的值,它始终在 rmNearest 上设置。” 在上述代码中发生任何运行之前,RoundingMode 始终为 rmNearest。

  2. 这不是真正的测试。这只是显示问题发生位置的代码。

添加了视频说明。

因此,为了努力改进我的问题,我决定添加显示我的奇怪问题的视频。这是生产代码,我只添加了用于检查 RoundingMode 的断言。 在视频的第一部分中,我展示了原始测试(@Sir Rufo、@Craig Young)、负责转换的方法以及我得到的正确结果。在第二部分中,我展示了当我添加另一个不相关的测试时,我得到了不正确的结果。视频可以找到here

添加了可重现的示例

这一切都归结为以下代码:

procedure FloatingPointNumberHorror;
const
  CStrGPS = 'N5145.37936E01511.8029';
var
  LLongitude: Integer;
  LFloatLon: Double;
  adcConnection: TADOConnection;
  qrySelect: TADOQuery;
  LCSVStringList: TStringList;
begin
  //Tested on Delphi 2007, 2009, XE 5 -  Windows 7 64 bit
  adcConnection := TADOConnection.Create(nil);
  qrySelect := TADOQuery.Create(adcConnection);
  LCSVStringList := TStringList.Create;
  try
    //Prepare on the fly csv file required by ADOQuery
    LCSVStringList.Add('Col1;Col2;');
    LCSVStringList.Add('aaaa;1234;');
    LCSVStringList.SaveToFile(ExtractFilePath(ParamStr(0)) + 'test.csv');

    qrySelect.CursorType := ctStatic;
    qrySelect.Connection := adcConnection;
    adcConnection.ConnectionString := 'Provider=Microsoft.Jet.OLEDB.4.0;Data Source='
      + ExtractFilePath(ParamStr(0)) + ';Extended Properties="text;HDR=yes;FMT=Delimited(;)"';

    // Real stuff begins here, above we have only preparation of environment.
    LFloatLon := 15 + 11.8029*1/60;
    LLongitude := Round(LFloatLon * 100000);
    Assert(LLongitude = 1519671, 'Asertion 1'); //Here you will NOT receive error.

    //This line changes the FPU control word from $1372 to $1272.
    //This causes the change of Precision Control Field (PC) from 3 which means
    //64bit precision to 2 which means 53 bit precision thus resulting in improper rounding?
    //--> ADODB.TParameters.InternalRefresh->RefreshFromOleDB -> CommandPrepare.Prepare(0)
    qrySelect.SQL.Text := 'select * from [test.csv] WHERE 1=1';

    LFloatLon := 15 + 11.8029*1/60;
    LLongitude := Round(LFloatLon * 100000);
    Assert(LLongitude = 1519671, 'Asertion 2'); //Here you will receive error.

  finally
    adcConnection.Free;
    LCSVStringList.Free;
  end;
end;

只需复制并粘贴此过程并添加ADODBto use 子句。该问题似乎是由 Delphi 的 ADO 包装器使用的某些 Microsoft COM 对象引起的。该对象正在更改 FPU 控制字,但它是不改变舍入模式。它正在改变精确控制。

以下是启动 ADO 相关方法之前和之后的 FPU 屏幕截图:

FPU screenshot

我想到的唯一解决方案是使用Get8087CW在使用 ADO 代码之前然后Set8087CW使用先前存储的控制世界来设置控制世界。


该问题很可能是因为代码中的其他内容正在更改浮点舍入模式。看看这个程序:

{$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.

输出是:



15.196715
1519671.5
1519671
1519671
1519672
1519671
  

显然,您的特定计算取决于浮点舍入模式以及实际输入值和代码。确实是文档确实表明了这一点:

Note:Round 的行为可能受到 Set8087CW 过程或 System.Math.SetRoundMode 函数的影响。

因此,您首先需要找到程序中正在修改浮点控制字的其他内容。然后,您必须确保每当行为不当的代码执行时都将其设置回所需的值。


恭喜您进一步调试了这一点。事实上它实际上是乘法

LLong*100000

受控制精度的影响。

要知道是这样,请看这个程序:

{$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
  

因此,精度控制会影响乘法的结果,至少在8087单元的80位寄存器中是这样。

编译器不会将该乘法的结果存储到变量中,而是保留在 FPU 中,因此这种差异会传递到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
  

注意乘法的结果是如何留在ST(0)因为那就是Round期望它的参数。

事实上,如果您将乘法拉到一个单独的语句中,并将其分配给一个变量,那么行为会再次变得一致:

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

上面的代码为两者产生相同的输出$1272 and $1372.

但仍然存在基本问题。您失去了对浮点控制状态的控制。为了解决这个问题,您需要保持对 FP 控制状态的控制。每当您调用可能修改它的库时,请在调用之前将其存储起来,然后在调用返回时恢复。如果您想要拥有可重复、可靠且健壮的浮点代码之类的东西,不幸的是,这种游戏是不可避免的。

这是我执行此操作的代码:

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;

请注意,此代码处理两个浮点单元,因此可以用于使用 SSE 单元而不是 8087 单元的 64 位。


无论如何,这是我的 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
  
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

浮点数转换恐怖,有出路吗? 的相关文章

随机推荐

  • PHP echo before sleep功能,不起作用

    我希望在睡眠函数执行之前在浏览器中输出回显 每次 以下代码不起作用 set time limit 0 ob implicit flush 1 ob start echo Start br ob flush for i 0 i lt 10 i
  • Redux VS Context API [关闭]

    Closed 这个问题是基于意见的 目前不接受答案 我非常熟悉 Context API 我完成了 Redux 速成课程 它们对我来说 原则上 很相似 问题是 我应该关注哪一个 Context API 和 Redux 之间的主要优缺点是什么
  • 在 Spark Scala 中合并两个 RDD

    我有两个 RDD rdd1 字符串 字符串 key1 value11 key2 value12 key3 value13 rdd2 字符串 字符串 key2 value22 key3 value23 key4 value24 我需要使用 r
  • 使用标签时如何使用Onclick事件

    我有两个java类 and 两种布局对于两个班级来说 每个layout正在拥有一个button在里面 两个班级都在延长Activity 现在在我使用的第一个布局中include像这样标记
  • 使用 Web API 在 jqGrid 中添加/编辑/删除

    我是 jqGrid 的新手 需要一些关于表单添加 编辑 删除功能的帮助 目前还没有找到相关资源 我的网格在添加 编辑时显示弹出窗口 还在单击编辑时填充数据 但是我不确定应该使用什么 javascript 代码来调用 Web api 来发布
  • scanf("%c", &c) 和 scanf(" %c", &c) 之间的区别[重复]

    这个问题在这里已经有答案了 考虑以下 C 代码片段 include
  • 如何在 PyTorch 中打印模型摘要?

    如何在 PyTorch 中打印模型的摘要 如下所示model summary 在 Keras 中执行的操作 Model Summary Layer type Output Shape Param Connected to
  • BackgroundWorker 从循环中执行 UI 更新

    我正在 BackgroundWorker 的 DoWork 内循环创建 ViewModel 对象 我报告每次迭代的进度 将新对象作为参数传递以由 ProgressChanged 处理程序 它是 UI 线程的朋友 检索 在该处理程序中 对象被
  • Windows Phone 7 列表框加载数据的进度条

    当列表框完成加载其数据时 是否有一个我可以监听的事件 我有一个文本框和一个列表框 当用户按 Enter 键时 列表框将填充来自 Web 服务的结果 我想在列表框加载时运行进度栏 并在完成后折叠它 UPDATE
  • javascript 字符串比较

    我有以下脚本 document write 12 lt 2 返回 true 有什么理由吗 文档说 javascript 以数字方式比较字符串 但是 我不明白 12 如何小于 2 JavaScript 逐个字符地比较字符串 直到其中一个字符不
  • 将日期从 Excel 转换为 R

    我很难将日期从 excel 从 csv 读取 转换为 R 非常感谢帮助 这是我正在做的事情 df date as Date df excel date format d m Y 但是 有些日期会被转换 但有些则不会 这是以下的输出 head
  • ggpairs 绘图,其中包含具有重要性星级和自定义主题的相关值热图

    我想用 ggPairs 创建一个相关图 其中应该包含 相关值的热图 就像在这个SO问题中一样 相关性的显着性星号 就像在这个SO问题中一样 根据自定义主题的字体类型和字体大小 基于 user20650对上述SO问题提供的优秀解决方案 我成功
  • Angular2:将表单上下文绑定到 ngTemplateOutlet

    我试图定义一个包含动态表单 使用 ReactiveForms 的组件 用户应该能够在其中添加 删除控件 控件可以采用多种形式 并且必须在组件外部定义 因此我认为 TemplateRef 最适合这种情况 我正在努力寻找一种通过使用 formC
  • XSL 与区域化/国际化数字格式

    在格式化数字时 XSL 中是否内置了任何区域化支持 目前 我的底层 XML 包含英国 美国格式的数字 例如 54321 12345 我可以对此进行选择总和 以相同的格式给出总计 我可以使用 format number 54321 12345
  • Lattice中的facet_wrap相当于什么

    假设我们有一些这样的数据 dta lt data frame group rep letters 1 8 each 1000 x runif 8000 y runif 8000 我想为每个组生成一个包含 y x 的格子图 但是 第一行有 a
  • 左外连接等效

    我有一个包含空值的表 在 ORDER 表中 PART ID 部分有 2 个空值 CUSTOMER ID 部分有 2 个空值 我有这样的疑问 SELECT O ORDER ID O ORDER DATE O CUST ID O QUANTIT
  • 将图形直接放入 Knit 文档中(不将其文件保存在文件夹中)

    我正在 RStudio 中创建一个名为 test Rnw 的文档 其 MWE 如下 documentclass 12pt english nohyper tufte handout usepackage tabularx usepackag
  • 逗号运算符的正确用法是什么?

    我看到了这段代码 if cond perror an error occurred exit 1 为什么要这么做 为什么不只是 if cond perror an error occurred exit 1 在你的例子中 它根本没有任何理由
  • Coq 将不存在的语句转换为 forall 语句

    我是 Coq 的新手 这是我的问题 我有一个声明说 H forall x term exists y term P x y P y x 我猜它相当于 forall x y term P x y P y x gt false 但我可以使用哪种
  • 浮点数转换恐怖,有出路吗?

    背景 最近 我的同事向我们的测试项目添加了一些新测试 其中之一还没有传递或持续集成系统 由于我们有大约 800 个测试 并且需要一个小时才能运行所有测试 因此我们经常会犯错误 并且只在我们的开发机器上运行我们当前已实现的测试 这种方法有其弱