5

Introduction

I encountered a problem with Currency in one of our applications. I was getting different results in Win32 and Win64. I found an article here that shows a similar problem but that one was fixed in XE6. The first thing I tried to do was create a MCVE to duplicate the problem. That's where the wheels fell off. What looks like the identical code in the MCVE produces a different result compared to the application. The generated code 64 bit is different. So my question morphed into why are they different and once I figure that out then I can create a suitable MCVE.

I have a method that is calculating a total. This method calls another method to get a value that needs to be added to the total. The method returns a single. I assign the single value to a variable and then add it to the total which is a Currency. In my main application the value for the total is used later on but adding that to the MCVE doesn't change the behavior. I made sure that the compiler options were the same.

In my main application, the result from the calculation is $2469.6001 in Win32 and 2469.6 in Win64 but I can't duplicate this in the MCVE. Everything on the Compiling options page was the same and optimizations were disabled.

Attempted MCVE

Here is the code for my attempted MCVE. This mimics the actions in the original application.

program Project4;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TTestClass = class
  strict private
    FMyCurrency: Currency;
    function GetTheValue: Single;
  public
    procedure Calculate;
    property MyCurrency: Currency read FMyCurrency write FMyCurrency;
  end;

procedure TTestClass.Calculate;
var
  myValue: Single;
begin
  FMyCurrency := 0.0;
  myValue := GetTheValue;
  FMyCurrency := FMyCurrency + myValue;
end;

function TTestClass.GetTheValue: Single;
var
  myValueExact: Int32;
begin
  myValueExact := 1159354778; // 2469.60009765625;
  Result := PSingle(@myValueExact)^;
end;

var
  testClass: TTestClass;
begin
  testClass := TTestClass.Create;
  try
    testClass.Calculate;
    WriteLn(CurrToStr(testClass.MyCurrency));
    ReadLn;
  finally
    testClass.Free;
  end;
end.

This code generates the following assembler for the last two lines of TTestClass.Calculate:

Project4.dpr.25: myValue := GetTheValue;
00000000004242A8 488B4D40         mov rcx,[rbp+$40]
00000000004242AC E83F000000       call TTestClass.GetTheValue
00000000004242B1 F30F11452C       movss dword ptr [rbp+$2c],xmm0
Project4.dpr.26: FMyCurrency := FMyCurrency + myValue;
00000000004242B6 488B4540         mov rax,[rbp+$40]
00000000004242BA 488B4D40         mov rcx,[rbp+$40]
00000000004242BE F2480F2A4108     cvtsi2sd xmm0,qword ptr [rcx+$08]
00000000004242C4 F3480F5A4D2C     cvtss2sd xmm1,qword ptr [rbp+$2c]
00000000004242CA F20F590D16000000 mulsd xmm1,qword ptr [rel $00000016]
00000000004242D2 F20F58C1         addsd xmm0,xmm1
00000000004242D6 F2480F2DC8       cvtsd2si rcx,xmm0
00000000004242DB 48894808         mov [rax+$08],rcx

Main Application

This is an extract from the main application. It's difficult to give more information but I don't think that will change the nature of the question. In this class, FBulkTotal is declared as a Currency that is strict private. UpdateTotals is public.

procedure TMainApplicationClass.UpdateTotals(aMyObject: TMyObject);
var
  bulkTotal: Single;
begin
  ..
        bulkTotal := grouping.GetTotal(aMyObject, Self);
        FBulkTotal := FBulkTotal + bulkTotal;
  ..
end;

The generated code for these two lines is:

TheCodeUnit.pas.7357: bulkTotal := grouping.GetTotal(aMyObject, Self);
0000000006DB0804 488B4D68         mov rcx,[rbp+$68]
0000000006DB0808 488B9598000000   mov rdx,[rbp+$00000098]
0000000006DB080F 4C8B8590000000   mov r8,[rbp+$00000090]
0000000006DB0816 E8551C0100       call grouping.GetTotal
0000000006DB081B F30F114564       movss dword ptr [rbp+$64],xmm0
TheCodeUnit.pas.7358: FBulkTotal := FBulkTotal + bulkTotal;
0000000006DB0820 488B8590000000   mov rax,[rbp+$00000090]
0000000006DB0827 488B8D90000000   mov rcx,[rbp+$00000090]
0000000006DB082E F3480F2A8128010000 cvtsi2ss xmm0,qword ptr [rcx+$00000128]
0000000006DB0837 F30F104D64       movss xmm1,dword ptr [rbp+$64]
0000000006DB083C F30F590D54020000 mulss xmm1,dword ptr [rel $00000254]
0000000006DB0844 F30F58C1         addss xmm0,xmm1
0000000006DB0848 F3480F2DC8       cvtss2si rcx,xmm0
0000000006DB084D 48898828010000   mov [rax+$00000128],rcx

What's strange is that the generated code is different. The MCVE has a cvtsi2sd followed by a cvtss2sd but that main application uses a movss in place of the cvtss2sd when copying the contents of the single value into the xmm1 register. I pretty sure that is what is causing the different result but without being able to create a MCVE, I can't even confirm that it is a problem with the compiler.

Question

My question is what can cause these differences in code generation? I assumed that the optimizations could do this type of thing but I made sure those were the same.

Graymatter
  • 6,529
  • 2
  • 30
  • 50
  • Win32 uses the FPU and internally, the 80 bit Extended type. Win64 uses SSE2 and that does not have such an Extended type for internal calculations, so results my be slightly different. I can't test this (no Win64 here), but I guess that that is the caue of the problem. Currency is just a scaled Int64, but mixing that with Single and/or Double can cause slightly different outcomes. If you really want accurcacy, use my [BigDecimal](http://rvelthuis.de/programs/bigdecimals.html) type throughout. – Rudy Velthuis Mar 28 '19 at 07:37
  • 1
    Currency is a somewhat crappy type. Better to use a proper decimal type. – David Heffernan Mar 28 '19 at 08:09
  • 1
    While this codegen situation is a compelling mystery, mixing currency and floating point types is an excellent way to destroy the precision that you are trying to retain by using the currency type, even when things are working as they're meant to. Why are you doing this in the first place? – J... Mar 28 '19 at 11:11
  • Note that if you explicitly put fp values into currency variables before performing operations with other currency values the compiler will always do the right thing (ie: cvtss2sd, divide by 10000, cvtsd2si, then add the currency values as integers). In both cases above it looks like the compiler is doing different but both wrong things - converting and adding the values as either singles or doubles before casting back to integer. Whether this is a bug or whether Emba just didn't think through how they wanted this type to work, I'm agreed with @DavidHeffernan that this is a bad data type. – J... Mar 28 '19 at 11:31

1 Answers1

-1

You should not be using any floating point type values when dealing with currency.

I recommend you watch Floating Point Numbers video from Computerphile where he explains of how floating point values are handled by computers and why they should not be used when handling currency. https://www.youtube.com/watch?v=PZRI1IfStY0

SilverWarior
  • 7,372
  • 2
  • 16
  • 22