gpt4 book ai didi

delphi - float 转换恐怖,有没有出路?

转载 作者:行者123 更新时间:2023-12-03 15:21:05 25 4
gpt4 key购买 nike

背景

最近,我的同事在我们的测试项目中添加了一些新测试。其中之一尚未通过或持续集成系统。由于我们有大约800个测试,并且要花所有时间来运行所有测试,因此我们经常会犯一个错误,并且仅在当前已实施的测试上运行在开发机上。这种方法有其缺点,因为有时会在本地通过测试,但随后在集成系统上会失败。当然,有人会说“这不是错误,测试应该相互独立!”。

当然,在理想世界中,但在我的世界中不是。在一个世界中,您在initialization部分中初始化了很多单例,而在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, 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上设置。

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

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


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

我猜必须存在一些导致问题的内存损坏,并且 SelectNextPage所在的行只能显示该问题。但是,在三台不同的计算机上也会发生相同的问题。

任何人都可以给我一个解决该问题的方法吗?或如何确保始终获得稳定的转换结果?

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


我已经检查了RoundingMode,并在前面提到过:“我已经四重检查SelectNextPage方法,它不会触发任何可能改变FPU单元工作方式的事件。它不会更改RoundingMode的值,它始终是在rmNearest上进行设置。”在上述代码中出现任何符文之前,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。似乎该问题是由Delphi的ADO包装器使用的某些Microsoft COM对象引起的。该对象正在更改FPU控制字,但未更改舍入模式。它正在改变精度控制。

这是激发ADO相关方法之前和之后的FPU屏幕截图:



我想到的唯一解决方案是在使用ADO代码之前先使用 Get8087CW,然后使用 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.1967151519671.51519671151967115196721519671

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


这受精度控制的影响。

要查看情况,请看以下程序:

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

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 @ROUND004060F9 52               push edx004060FA 50               push eax004060FB A1107A4000       mov eax,[$00407a10]00406100 E827F0FFFF       call @Write0Int6400406105 E87ADEFFFF       call @WriteLn0040610A 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);


上面的代码为 $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 - float 转换恐怖,有没有出路?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20521608/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com