背景

最近,我的同事在我们的测试项目中添加了一些新测试.其中一个没有传递或持续集成系统.由于我们有大约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 $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;

只需复制并粘贴此过程并将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.196715
1519671.5
1519671
1519671
1519672
1519671

显然,您的特定计算取决于浮点舍入模式以及实际输入值和代码.事实上,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($1272);
  e1 := d * 100000;
  Set8087CW($1372);
  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 [$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美元和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.

产量

1519672
1519671

delphi – 浮点数转换恐怖,还有出路吗?的更多相关文章

  1. ios – 如何在运行calabash测试时模拟后端交互

    解决方法由于直到今天我才收到其他反馈,我将回答我自己的问题.我们选择了为后端创建一个非常简单的模拟的方法.我们使用了Sinatra,但node.js或类似技术会产生相同的结果.可以通过简单的RESTAPI控制模拟.在步骤定义中,我们为运行场景适当地配置了模拟后端.这有点开销,因为模拟必须与真正的后端一起发展,但直到今天它仍然像一个强大的解决方案.

  2. 从IOS / iPad / iPhone的最大速度

    我使用OpenCVforiOS完成计算密集型应用程序.当然这很慢.但它比我的PC原型慢了200倍.所以我正在优化它.从最初的15秒,我能够获得0.4秒的速度.我想知道我是否找到了所有的东西以及别人想要分享的东西.我做了什么:>将OpenCV中的“double”数据类型替换为“float”.双倍是64位,32位cpu不能轻易处理,所以浮动给了我一些速度.OpenCV经常使用双倍.>为编译器选项添加了

  3. 如何在iOS中使用GL_HALF_FLOAT_OES类型的纹理?

    谢谢!

  4. 在iOS ARM设备(iPhone 4)上支持非正常IEEE 754浮点数

    >可以将iOS系统设置为提供对正常数量的支持,而不要求编译器仅生成完整的软件浮点代码?是.这可以通过将FPSCR中的FZ位设置为零来实现:请注意,当遇到可观量的非正常值时,这可能会导致应用程序性能的显着下降.您可以恢复默认浮点状态,然后再调用任何不使ABI保证在非默认模式下正常工作的代码:请提交bugreport请求,为iOS中的FP操作模式提供更好的文档.

  5. ios – 我在哪里可以找到用于创建IPad应用程序的Delphi资源?

    我之前一直在使用Delphi并且一直都是Windows家伙.我的妻子为我的生日买了一台新的iPad,我昨晚第一次使用它.哇!…

  6. ios – 舍入集合视图的浮点值时出错

    我想在我的设备上摆脱一个像素问题.我正在设计一个如下所示的日历我正在为此目的使用UICollectionViewCell问题是,如果我增加宽度甚至0.0000001点,那么每行绘制的单元格数量是6而不是7.我已尝试增加UIEdgeInset(0.00001),但这是同样的问题.流布局的属性已正确设置.如何在不留空隙的情况下使单元尺寸保持一致?电池的实际宽度为53.571428571428569(3

  7. 如何从命令行部署OSX或IOS Delphi项目?

    我正在使用像这样的脚本构建我的Delphi应用程序现在我想添加一个选项将应用程序部署到OSX系统修改这样的脚本,那么可以从命令行部署OSX或IOSDelphi项目吗?

  8. Swift教程05-基本数据类型(一)整型浮点型

    Swift的基本数据类型与Oc的数据类型有一些不同;除了写法改变了一些之外,还增加了一些类型,下面来由浅入深的介绍其基本数据类型注意:基本数据类型以大写字母开头,不同于C语言!

  9. swift基本语法—变量和常量—整型-浮点型 -布尔型 bool

    swift基本语法——变量和常量swift语言中声明变量使用var关键字,声明常量使用let关键字importFoundationvarStr="helloworld"//Str是变量varnum:StringletInstrationCount:Int=2//InstrationCount是常量vartemp:Float32=2.8//Str=1024//编译会失败,不能修改常量//varNum

  10. Swift学习笔记二十三——Swift泛型初识

    泛型的概念在Java中也是存在的,泛型可以使代码更为精炼,是对数据类型使用的一种优化。如果此时又多了一个需求,需要判断两个浮点型的是否相等。那怎么办,代码实现基本和上面一样,不过我们又得重新写一遍浮点型的代码:。以上两个代码将会导致代码可重用性变差且变得冗余,如果使用泛型来实现,将会变得很简单:.泛型的实现是不是很高大上呢。让我们的代码也提升B格吧。

随机推荐

  1. delphi – 主窗口按进程名称处理

    DelphiXe,Win7x64如何从进程名称(exe文件的完整路径)获取主窗口句柄,或至少一个类或窗口名称(如果该进程只有一个窗口).例:解决方法我同意Petesh的说法,你需要枚举顶级窗口并检查创建它的进程的模块文件名.为了帮助您开始枚举顶级窗口,这是一个delphi实现.首先,当你回调给你时,你需要一些与EnumWindows方法通信的方式.为此声明一条记录,该记录将保存您要查找的模块的文件

  2. 如何在Delphi中纯粹通过RTTI信息(即不使用任何实际对象实例)获取TObjectList的子项类型?

    我正在使用RTTI实现用于流式传输任意Delphi对象的通用代码,并且为了使其工作(更具体地说,为了使加载部分工作),我需要以某种方式获得TObjectList的子项类型<T>不使用任何实际对象实例的字段.要求不使用任何实际对象实例的明显原因是,在从流加载对象的情况下(仅基于要加载的对象的类类型的知识),我将不会有任何实例在加载完成之前完全可用–我宁愿只能访问相关类的纯RTTI数据.我希望能

  3. inno-setup – Inno Setup – 安装程序背景图片

    图像作为安装程序背景如何用inno5.5.9做到这一点?

  4. inno-setup – Inno Setup – 如何添加多个arc文件进行解压缩?

    使用InnoSetup解压缩弧文件.我希望有可能解压缩多个arc文件以从组件选择中安装文件(例如).但仍然显示所有提取的整体进度条.这可能吗?的回答的修改预备是相同的,参考其他答案.在ExtractArc中,为要提取的每个存档调用AddArchive.

  5. delphi – 如何在DataSet的帮助下在TAdvStringGrid中显示数据库中的BLOB图像

    解决方法CreateBlobStream正在创建一个TStream对象,而不是TMemoryStream.由于您不想将JPG写入数据库,因此应使用bmRead而不是bmReadWrite.我不习惯sqlite,但你必须确保使用合适的二进制日期类型.为了确保存储的图像真的是JPG,您应该编写JPG以进行测试,例如:

  6. inno-setup – 在Inno Setup的Code部分下载程序后运行程序

    如何运行我通过Internet下载的应用程序,在代码部分中使用,并等待该应用程序完成运行.我有,使用InnoTools下载程序,下载这两个文件,我想,在第二个完成下载后运行该下载,或jdk-8u111-windows-x64.exe,然后继续安装.解决方法使用其他下载插件,而不是ITD(请参阅下面的原因).例如,InnoDownloadPlugin.当您包含idp.iss时,它定义了一个全局IDP

  7. progress-bar – Inno Setup Run部分的简单进度页面

    我的安装程序非常简单,它基本上是:>欢迎页面>进展页面>最终页面欢迎页面和最终页面是标准页面.在Progress页面,我正在静默安装一堆其他程序.实际的脚本是在[Run]部分中安装每个程序.问题是酒吧达到100%然后停留在那里.我只能更改消息文本.我想要实现的是使用Pascal脚本显示进度,例如:这样我就可以显示更准确的进度条.这就是我所拥有的:问题是,当我构建安装程序时,它不显示欢迎页面.我做错了什么?

  8. delphi – 如何使“显示/隐藏桌面图标”设置生效?

    下面的代码调用SHGetSetSettings函数来隐藏桌面图标但它只是从视图菜单中取消选中“显示桌面图标”.我打电话给SHChangeNotify;更新桌面,但这不起作用?解决方法isa,要刷新桌面,您可以将F5键发送到progman窗口隐藏桌面图标的另一种方法是再次显示

  9. inno-setup – Inno Setup – 避免显示子安装程序的文件名

    我试图使用InnoSetup–Howtohidecertainfilenameswhileinstalling?(FilenameLabel)的想法Theonlysuresolutionistoavoidinstallingthefiles,youdonotwanttoshow,usingthe[Files]section.Installthemusingacodeinstead.UsetheEx

  10. inno-setup – Inno Setup磁力链接下载实施

    我目前正在使用InnoDownloadPlugin为我的安装程序下载文件,这个问题最大的问题是faila正确下载文件.因为连接不良等诸多原因.我想添加一种替代方法来下载文件,因此用户可以选择是否需要常规方式或torrent方式.我知道我可以使用aria2c.exe应用程序(https://aria2.github.io/),有人可以帮我实现它的inno设置代码吗?我需要的是使用torrent(ar

返回
顶部