Rounding error using TDateTime on iOS

When calculating a 32-bit identifier from a timestamp (TDateTime), I get a strange error. In some situations, the meaning is different for different processors.

The fTimeStamp field fTimeStamp read from the Double field in the SQLite database. The code below computes the 32-bit identifier (lIntStamp) from fTimeStamp , but in some (rare) situations the value is different on different computers, even if the original database file is exactly the same (i.e. Double is stored in the file the same way).

 ... 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; 

The accuracy of TDateTime (Double) is 15 digits, but the rounded value in the code uses only 11 digits, so there should be enough information for proper rounding.

To mention an example of values: in a particular test run, the lIntStamp value was $ 74AE699B on a Windows computer and $ 74AE699A on an iPad (= only the last bit is different).

Is the Round function implemented on each platform different?

PS. Our target platforms are currently Windows, MacOS, and iOS.

Edit:

I made a small test program based on comments:

 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 output:

 36317325723 36317325722.5 36317325722 

On iPad, the conclusion is:

 36317325722 36317325722.5 36317325722 

The difference is in the first number, which shows the rounding off of the intermediate calculation, so the problem arises because x86 has higher internal accuracy (80 bits) than ARM (64 bits).

+6
source share
2 answers

Assuming all processors are compatible with IEEE754, and that you use the same rounding mode on all processors, you can get the same results from all different processors.

However, there may be compiled code differences or implementation differences with your code in its current form.

Consider how

 fTimeStamp * 24 * 60 * 60 * 10 

. Some compilers may execute

 fTimeStamp * 24 

and then save the intermediate result in the FP register. Then multiply this by 60 and save to the FP register. And so on.

Now, in x86, floating point registers are extended to 80 bits, and by default, these intermediate registers will hold results up to 80 bits.

ARM processors, on the other hand, do not have 80 registers. Intermediate values ​​are stored with a double precision of 64 bits.

So the difference in the implementation of the machine explains your behavior.

Another possibility is that the ARM compiler puts a constant in the expression and evaluates it at compile time, reducing the above to

 fTimeStamp * 864000 

I have never seen an x86 or x64 compiler that does this, but perhaps an ARM compiler. This is the difference in the compiled code. I am not saying that this is happening; I do not know mobile compilers. But there is no reason why this could not happen.

However, here is your salvation. Record your expression as above with one multiplication. Thus, you get rid of any area for storing intermediate values ​​with different accuracy. Then, while Round means the same on all processors, the results will be identical.

Personally, I would avoid questions in rounding mode, and use Trunc instead of Round . I know this has a different meaning, but for your purposes it is an arbitrary choice.

After that you will be left with:

 lIntStamp := Trunc(fTimeStamp * 864000); //steps of 1/10th second lIntStamp := lIntStamp and $FFFFFFFF; 

If Round behaves differently on different platforms, you may need to implement it yourself on ARM. In x86, the default rounding mode is bankers. It matters only halfway between two integers. Therefore, check if Frac(...) = 0.5 and round accordingly. This check is safe because 0.5 is accurately representable.

On the other hand, you seem to argue that

 Round(36317325722.5000008) = 36317325722 

on ARM. If so, this is a mistake. I can’t believe what you say. I believe that the value passed to Round is actually 36317325722.5 on ARM. This is the only thing that can make sense to me. I can't believe Round faulty.

+5
source

To be complete, here's what happens:

Call Round(d*n); , where d is a double and n is a number, turns the multiplication into an extended value before calling the Round function in x86. On x64 or OSX or IOS / Android, there is no progress to the 80-bit extended value.

Parsing extended values ​​can be difficult because RTL does not have a function to record the full accuracy of the extended value. John Herbster wrote such a library http://cc.embarcadero.com/Item/19421 . (Add FormatSettings in two places to compile it on a modern version of Delphi).

Below is a small test that records the results of extended and double values ​​in 1-bit increments in the input double value.

 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 

Note that the fTimeStamp value in question has an exact double representation (ending in .5) when using double precision calculation, while advanced calculation gives a value that is slightly higher. This is an explanation of the various rounding results for platforms.


As noted in the comments, the solution will store the calculation in double before rounding. This will not solve the backward compatibility problem, which is not easy to accomplish. Perhaps this is a good opportunity to save time in a different format.

+2
source

Source: https://habr.com/ru/post/986642/


All Articles