18

When compiled with Delphi 2009 and run, this console application writes "strange". The values on both sides of the "less than" operator are equal, but the code behaves as if they are not equal. What can I do to avoid this problem?

program Project5;

{$APPTYPE CONSOLE}

var
  C: Currency;
begin
  C := 1.32;

  if C < 1.32 then
  begin
    WriteLn('strange');
  end;

  ReadLn;
end.

p.s. code works fine with other values.

This answer by Barry Kelly explains that the Currency type "is not susceptible to precision issues in the same way that floating point code is."

Cœur
  • 37,241
  • 25
  • 195
  • 267
mjn
  • 36,362
  • 28
  • 176
  • 378
  • 1
    see also http://stackoverflow.com/questions/11929457/abs-function-failing-in-delphi – kludg Jan 16 '13 at 12:51
  • I assume the comparison has to be between real values, so C is converted to real before the comparison and all the 'floating point comparison 'issues apply. – Jan Doggen Jan 16 '13 at 13:07
  • sorry, only found german explanation http://www.delphi-treff.de/tutorials/sonstiges/delphi-intern/floats-und-der-verwandte-currency/ – bummi Jan 16 '13 at 13:28
  • 2
    I don't understand, I see 13200 in ST0 and 13200 in ST1 and yet FCOMPP raises C0. Also why does having more decimal places generate different code? Use '1.321' instead of '1.32' in the example, and the result is not *strange*. – Sertac Akyuz Jan 16 '13 at 13:37
  • @JanDoggen Actually it's the other way around. The literal `1.32` is converted to `Currency` and then 13200 compared with 13200. – David Heffernan Jan 16 '13 at 15:10
  • To make it even more strange replace `<` by `<>` in OP code – kludg Jan 16 '13 at 15:32
  • @Serg That doesn't seem at all strange. – David Heffernan Jan 16 '13 at 16:09
  • 1
    @SertacAkyuz Turns out that the literal is actuall just ever so slightly greater than 13200, but the FPU window can't show that! – David Heffernan Jan 16 '13 at 16:50
  • 1
    @David - Thanks, and my respects for reaching a conclusion on what seems to be a difficult debug job. – Sertac Akyuz Jan 16 '13 at 17:04

4 Answers4

9

This would appear to be a regression in Delphi.

The output is 'strange' in Delphi 2010. But in XE2 there is no output, and so the bug is not present. I don't have XE at hand to test on, but thanks to @Sertac for confirming that XE also outputs 'strange'. Note that older versions of Delphi are also fine, so this was a regression around D2009 time.

On 2010 the code generated is:

Project106.dpr.10: if C < 1.32 then
004050D6 DB2D18514000     fld tbyte ptr [$00405118]
004050DC DF2D789B4000     fild qword ptr [$00409b78]
004050E2 DED9             fcompp 
004050E4 9B               wait 
004050E5 DFE0             fstsw ax
004050E7 9E               sahf 
004050E8 7319             jnb $00405103
Project106.dpr.12: WriteLn('strange');

The literal 1.32 is stored as a 10 byte floating point value that should have the value 13200. This is an exactly representable binary floating point value. The bit pattern for 13200 stored as 10 byte float is:

00 00 00 00 00 00 40 CE 0C 40

However, the bit pattern stored in the literal at $00405118 is different, and is slightly greater than 13200. The value is:

01 00 00 00 00 00 40 CE 0C 40

And that explains why C < 1.32 evaluates to True.

On XE2 the code generated is:

Project106.dpr.10: if C < 1.32 then
004060E6 DF2DA0AB4000     fild qword ptr [$0040aba0]
004060EC D81D28614000     fcomp dword ptr [$00406128]
004060F2 9B               wait 
004060F3 DFE0             fstsw ax
004060F5 9E               sahf 
004060F6 7319             jnb $00406111
Project106.dpr.12: WriteLn('strange');

Notice here that the literal is held in a 4 byte float. This can be seen by the fact that we compare against dword ptr [$00406128]. And if we look at the contents of the single precision float stored at $00406128 we find:

00 40 4E 46

And that is exactly 13200 as represented as a 4 byte float.

My guess is that the compiler in 2010 does the following when faced with 1.32:

  • Convert 1.32 to the nearest exactly representably 10 byte float.
  • Multiply that value by 10000.
  • Store the resulting 10 byte float away at $00405118.

Because 1.32 is not exactly representable, it turns out that the final 10 byte float is not exactly 13200. And presumably the regression came about when the compiler switch from storing these literals in 4 byte floats to storing them in 10 byte floats.

The fundamental problem is that Delphi's support for the Currency data type is founded on an utterly flawed design. Using binary floating point arithmetic to implement a decimal fixed point data type is simply asking for trouble. The only sane way to fix the design would be to completely re-engineer the compiler to use fixed point integer arithmetic. It's rather disappointing to note that the new 64 bit compiler uses the same design as the 32 bit compiler.

To be quite honest with you, I would stop the Delphi compiler doing any floating point work with Currency literals. It's just a complete minefield. I would do the 10,000 shift in my head like this:

function ShiftedInt64ToCurrency(Value: Int64): Currency;
begin
  PInt64(@Result)^ := Value;
end;

And then the calling code would be:

C := 1.32;
if C < ShiftedInt64ToCurrency(13200) then
  Writeln ('strange');

There's no way for the compiler to screw that up!

Humph!

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 1
    I don't understand why `fld`, `fild` and `fcompp` is faulty code. – kludg Jan 16 '13 at 15:35
  • 1
    Actually the 'correct' code compares currency with single (`dword ptr` ) while 'faulty' code compares with extended (`tbyte ptr`). That is the difference, not `fld` etc – kludg Jan 16 '13 at 15:47
  • @Serg No, that's not it at all. What you have observed is that one version stores the literal as Single, and the other stores it as Extended. But either way, the value is `13200`. – David Heffernan Jan 16 '13 at 16:00
  • @Serg Do you have any idea why Currency operates on 64 bit integers using the floating point hardware unit? – David Heffernan Jan 16 '13 at 16:18
  • @Serg You did put me on the right track though. It turns out that the Delphi compiler fails to convert 1.32 to 13200. Not sure why yet. – David Heffernan Jan 16 '13 at 16:37
  • The currency is probably using the `Comp` type, which is supported by the x87 floating point cpu. I suppose this form of arithmetic was faster than using int64 in the turbo pascal days. – LU RD Jan 16 '13 at 16:56
  • @DavidHeffernan yes the final problem is this last bit in Extended value. Strange bug in codegen, probably related to poor `Currency` type implementation in the compiler. – kludg Jan 16 '13 at 17:02
5

As fas as hard casting like Currency(1.32) is not possible, you could use the following for explicit casting

Function ToCurrency(d:Double):Currency;
    begin
       Result := d;
    end;

procedure TForm1.Button1Click(Sender: TObject);

var
  C: Currency;

begin
  C := 1.32;
  if C < ToCurrency(1.32) then
  begin
    Writeln ('strange');
  end;
end;

another way could by forcing the usage of curreny by usage of a const or variable

const
  comp:Currency=1.32;
var
  C: Currency;
begin
  C := 1.32;
  if C < comp then
  begin
    writeln ('strange');
  end;
end;
bummi
  • 27,123
  • 14
  • 62
  • 101
  • @DavidHeffernan I agree, but fld is not used with both code variants above. – bummi Jan 16 '13 at 15:35
  • +1 and thank you for this (now accepted) solution: using const is the IMHO easiest way around this really nasty bug – mjn Jan 18 '13 at 12:18
  • 1
    Using a typed constant is a good approach since it forces the compiler to generate the integer 13200 rather than a floating point value – David Heffernan Jan 18 '13 at 12:28
2

To avoid this problem (bug in compiler) you can do as @bummi suggests, or try this run time cast:

if C < Currency(Variant(1.32)) then

To avoid the roundtrip into the FPU (and rounding errors), consider using this comparison function:

function CompCurrency(const A,B: Currency): Int64;
var
  A64: Int64 absolute A; // Currency maps internally as an Int64
  B64: Int64 absolute B;
begin
  result := A64-B64;
end;
...
if CompCurrency(C,1.32) < 0 then
begin
  WriteLn('strange');
end;

See this page for more information, Floating point and Currency fields.

LU RD
  • 34,438
  • 5
  • 88
  • 296
0

To add to David's answer - the next code is not strange, though equivalent to OP code:

program Project2;

{$APPTYPE CONSOLE}

var
  I: Int64;
  E: Extended;

begin
  I:= 13200;
  E:= 13200;
  if I < E then
  begin
    WriteLn('strange');
  end;
  ReadLn;
end.

Now the compiler generates correct binary value for Extended(13200), so the problem seems to be related to bad Currency type implementation in Delphi compiler.

kludg
  • 27,213
  • 5
  • 67
  • 118