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

delphi – 浮点数转换恐怖,还有出路吗?,第1张

概述背景 最近,我的同事在我们的测试项目中添加了一些新测试.其中一个没有传递或持续集成系统.由于我们有大约800个测试,并且运行所有这些测试需要一个小时,因此我们经常会犯错误,只在我们的开发机器上运行我们当前实施的测试.这种方法有其弱点,因为有时测试是在本地传递但在集成系统上失败.当然,有人可以说“这不是一个错误,测试应该是彼此独立的!”. 在理想的世界……当然,但不是在我的世界.不是在初始化部分初始 背景

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

在理想的世界……当然,但不是在我的世界.不是在初始化部分初始化了很多单例的世界中,Delphi本身引入了很多全局变量,后台初始化了OTL线程池,DevExpress方法连接到控件以进行绘制……还有其他几十个我不知道的事情.因此,在最终结果中,一个测试可以改变其他测试的行为. (当然这本身就很糟糕,我很高兴它会发生,因为希望我能够修复另一个依赖).

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

问题

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

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,LFS );  LTmpStr := copy(LStrLong,4,10);  LLong := LLong + StrTofloatDef( LTmpStr,LFS)*1/60;  LLongitude := Round(LLong * 100000);  CheckEquals(1519671,LLongitude);end;

问题是LLongitude有时等于1519671,有时它给出1519672.并且它是否给出1519672是否依赖于其他完全无关的代码片段在不同的测试中的不同方法:

FormXtrMainimport.JvWizard1.SelectNextPage;

我检查了SelectNextPage方法的四倍,它不会触发任何可能改变FPU单元工作方式的事件.它不会更改RoundingMode的值,它总是在rmNearest上设置.

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

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

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

我想必须有一些损坏的内存导致问题,SelectNextPage行只显示它.但是在三台不同的机器上会出现同样的问题.

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

对那些误解我的问题的人

>我已经检查了RoundingMode并且我之前提到过它:“我已经检查了SelectNextPage方法的四倍,它不会触发任何可能改变FPU单元工作方式的事件.它不会改变RoundingMode的值它是总是设在rmNearest上.“在上述代码中出现任何runding之前,RoundingMode始终是rmNearest.
>这不是真正的考验.这只是显示问题发生位置的代码.

视频说明已添加.

因此,在努力改进我的问题时,我决定添加显示我的眩晕问题的视频.这是生产代码,我只添加断言来检查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 72 to 72.    //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;

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

这是启动ADO相关方法之前和之后的FPU截图:

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

解决方法 问题很可能是因为代码中的其他内容正在改变浮点舍入模式.看看这个程序:
{$APPTYPE CONSolE}{$R *.res}uses  SysUtils,Math;const  CStrGPS = 'N5145.37936E01511.8029';var  LLatitude,LTmpStr: String;  LFS: TFormatSettings;begin  FillChar(LFS,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.1967151519671.51519671151967115196721519671

显然,您的特定计算取决于浮点舍入模式以及实际输入值和代码.事实上,documentation确实证明了这一点:

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

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

恭喜您进一步调试.实际上它实际上是乘法

LLong*100000

受精度控制的影响.

要查看是这样,请查看此程序:

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

产量

FALSE

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

编译器不会将该乘法的结果存储到变量中,而是保留在FPU中,因此这种差异会流向Round.

Project1.dpr.9: Writeln(Round(LLong*100000));004060E8 DD05A0AB4000     fld qword ptr [
tmp := LLong*100000;LLongitude := Round(tmp);
40aba0]004060EE D80D84614000 fmul DWord ptr [
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 statefinally  RestoreFPControlState(FPControlState);end;
406184]004060F4 E8BBCDFFFF call @ROUND004060F9 52 push edx004060FA 50 push eax004060FB A1107A4000 mov eax,[
{$APPTYPE CONSolE}var  d: Double;begin  d := 15.196715;  Set8087CW(72);  Writeln(Round(d * 100000));  Set8087CW(72);  Writeln(Round(d * 100000));  Readln;end.
407a10]00406100 E827F0FFFF call @Write0Int6400406105 E87ADEFFFF call @WriteLn0040610A E851CCFFFF call @_IOTest

注意乘法的结果如何保留在ST(0)中,因为这正是Round期望其参数的位置.

实际上,如果将乘法拉入单独的语句并将其分配给变量,则行为将再次变为一致:

15196721519671

上面的代码为1272美元和1372美元产生相同的输出.

尽管存在基本问题.您已失去对浮点控制状态的控制.要解决这个问题,你需要控制你的FP控制状态.每当您调用可能修改它的库时,请在调用之前将其存储起来,然后在调用返回时恢复.如果你想拥有可重复,可靠和强大的浮点代码,不幸的是,这种游戏是不可避免的.

这是我的代码:

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

值得一提的是,这是我的SSCCE:

产量

总结

以上是内存溢出为你收集整理的delphi – 浮点数转换恐怖,还有出路吗?全部内容,希望文章能够帮你解决delphi – 浮点数转换恐怖,还有出路吗?所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1279266.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-09
下一篇 2022-06-09

发表评论

登录后才能评论

评论列表(0条)

保存