在iOS上使用TDateTime进行舍入错误

时间:2015-05-06 10:04:33

标签: sqlite delphi firemonkey delphi-xe7

从时间戳(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位)。

2 个答案:

答案 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中。这不能解决后向兼容性问题,这不容易实现。 也许这是以另一种格式存储时间的好机会。