从时间戳(TDateTime)计算32位ID时,我得到一个奇怪的错误。在某些情况下,不同处理器的值会有所不同。
从SQLite数据库中的fTimeStamp
字段读取Double
字段。
下面的代码从fTimeStamp
计算32位ID(lIntStamp),但在某些(罕见)情况下,即使源数据库文件完全相同(即文件中存储的Double是相同)。
...
fTimeStamp: TDateTime
...
var
lIntStamp: Int64;
begin
lIntStamp := Round(fTimeStamp * 864000); //86400=24*60*60*10=steps of 1/10th second
lIntStamp := lIntStamp and $FFFFFFFF;
...
end;
TDateTime
(Double)的精度为15位,但代码中的舍入值仅使用11位数,因此应该有足够的信息正确舍入。
提及值的示例:在特定测试运行中,lIntStamp
的值在Windows计算机上为$ 74AE699B,在iPad上为$ 74AE699A(=最后一位不同)。
每个平台上的Round
功能是否实现不同?
PS。我们的目标平台目前是Windows,MacOS和iOS。
修改
我根据评论做了一个小测试程序:
var d: Double;
id: int64 absolute d;
lDouble: Double;
begin
id := $40E4863E234B78FC;
lDouble := d*864000;
Label1.text := inttostr(Round(d*864000))+' '+floattostr(lDouble)+' '+inttostr(Round(lDouble));
end;
Windows上的输出是:
36317325723 36317325722.5 36317325722
在iPad上,输出为:
36317325722 36317325722.5 36317325722
区别在于第一个数字,它显示了中间计算的舍入,因此问题发生是因为x86具有比ARM(64位)更高的内部精度(80位)。
答案 0 :(得分:5)
假设所有处理器都符合IEEE754标准,并且您在所有处理器中使用相同的舍入模式,那么您将能够从所有不同的处理器获得相同的结果。
但是,可能存在编译的代码差异,或者与代码的实现差异。
考虑如何
fTimeStamp * 24 * 60 * 60 * 10
评估。有些编译器可能会执行
fTimeStamp * 24
然后将中间结果存储在FP寄存器中。然后将其乘以60,并存储到FP寄存器。等等。
现在,在x86下,浮点寄存器是80位扩展的,默认情况下,这些中间寄存器将结果保持为80位。
另一方面,ARM处理器没有80个寄存器。中间值保持64位双精度。
这是一个机器实现差异,可以解释您观察到的行为。
另一种可能性是ARM编译器在表达式中发现常量并在编译时对其进行求值,将上述内容减少到
fTimeStamp * 864000
我从未见过这样做的x86或x64编译器,但也许是ARM编译器。这是编译代码的不同之处。我不是说它发生了,我不知道移动编译器。但没有理由不能发生这种情况。
然而,这是你的救赎。使用该单次乘法重写上面的表达式。这样你就可以摆脱存储到不同精度的中间值的任何范围。然后,只要Round
在所有处理器上的含义相同,结果就会相同。
就个人而言,我会避免有关舍入模式的问题而不是Round
会使用Trunc
。我知道它有不同的含义,但为了你的目的,这是一个任意的选择。
然后你会离开:
lIntStamp := Trunc(fTimeStamp * 864000); //steps of 1/10th second
lIntStamp := lIntStamp and $FFFFFFFF;
如果Round
在不同平台上的行为不同,那么您可能需要在ARM上自行实现它。在x86上,默认的舍入模式是银行家。这只有在两个整数之间的中间才有意义。因此,请检查是否Frac(...) = 0.5
并相应地进行舍入。该检查是安全的,因为0.5
完全可以表示。
另一方面,你似乎声称
Round(36317325722.5000008) = 36317325722
ARM上的。如果是这样的话就是一个bug。我无法相信你的要求。我相信传递给Round
的价值实际上是
ARM上的36317325722.5
。这是对我来说唯一有意义的事情。我无法相信Round
有缺陷。
答案 1 :(得分:2)
为了完成,这是正在发生的事情:
对Round(d*n);
的调用,其中d是double,n是数字,在x86环境中调用Round
函数之前将乘法转换为扩展值。在x64平台或OSX或IOS / Android平台上,没有促销到80位扩展值。
分析扩展值可能很棘手,因为RTL没有函数来写入扩展值的完整精度。
John Herbster写了这样一个图书馆http://cc.embarcadero.com/Item/19421。 (在两个地方添加FormatSettings
以使其在现代Delphi版本上编译)。
这是一个小测试,它在输入double值的1位变化步骤中写入扩展值和double值的结果。
program TestRound;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
ExactFloatToStr_JH0 in 'ExactFloatToStr_JH0.pas';
var
// Three consecutive double values (binary representation)
id1 : Int64 = $40E4863E234B78FB;
id2 : Int64 = $40E4863E234B78FC; // <-- the fTimeStamp value
id3 : Int64 = $40E4863E234B78FD;
// Access the values as double
d1 : double absolute id1;
d2 : double absolute id2;
d3 : double absolute id3;
e: Extended;
d: Double;
begin
WriteLn('Extended precision');
e := d1*864000;
WriteLn(e:0:8 , ' ', Round(e), ' ',ExactFloatToStrEx(e,'.',#0));
e := d2*864000;
WriteLn(e:0:8 , ' ', Round(e),' ', ExactFloatToStrEx(e,'.',#0));
e := d3*864000;
WriteLn(e:0:8 , ' ', Round(e),' ', ExactFloatToStrEx(e,'.',#0));
WriteLn('Double precision');
d := d1*864000;
WriteLn(d:0:8 , ' ', Round(d),' ', ExactFloatToStrEx(d,'.',#0));
d := d2*864000;
WriteLn(d:0:8 , ' ', Round(d),' ', ExactFloatToStrEx(d,'.',#0));
d := d3*864000;
WriteLn(d:0:8 , ' ', Round(d),' ', ExactFloatToStrEx(d,'.',#0));
ReadLn;
end.
Extended precision
36317325722.49999480 36317325722 +36317325722.499994792044162750244140625
36317325722.50000110 36317325723 +36317325722.500001080334186553955078125
36317325722.50000740 36317325723 +36317325722.500007368624210357666015625
Double precision
36317325722.49999240 36317325722 +36317325722.49999237060546875
36317325722.50000000 36317325722 +36317325722.5
36317325722.50000760 36317325723 +36317325722.50000762939453125
请注意,当使用双精度计算时,问题中的fTimeStamp
值具有精确的双重表示(以.5结尾),而扩展计算则提供稍微高一点的值。这是对平台不同舍入结果的解释。
如评论中所述,解决方案是在舍入之前将计算存储在Double中。这不能解决后向兼容性问题,这不容易实现。 也许这是以另一种格式存储时间的好机会。