【问题标题】:Strange results with currency value / constant value comparison货币价值/恒定价值比较的奇怪结果
【发布时间】:2013-01-16 12:23:31
【问题描述】:

当用 Delphi 2009 编译并运行时,这个控制台应用程序写“奇怪”。 “小于”运算符两边的值相等,但代码表现得好像它们不相等。我该怎么做才能避免这个问题?

program Project5;

{$APPTYPE CONSOLE}

var
  C: Currency;
begin
  C := 1.32;

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

  ReadLn;
end.

附言代码适用于其他值。

Barry Kelly 的 answer 解释说 Currency 类型“不像浮点代码那样容易受到精度问题的影响”。

【问题讨论】:

  • 我假设比较必须在实数值之间进行,因此在比较之前将 C 转换为实数值,并且所有“浮点比较”问题都适用。
  • 我不明白,我在 ST0 中看到 13200,在 ST1 中看到 13200,但 FCOMPP 提高了 C0。另外为什么有更多的小数位会产生不同的代码?示例中使用 '1.321' 而不是 '1.32',结果并不奇怪
  • @SertacAkyuz 原来文字实际上只是比 13200 稍大一点,但 FPU 窗口无法显示!
  • @David - 谢谢,我很敬意就似乎很难的调试工作得出结论。

标签: delphi compare delphi-2009


【解决方案1】:

这似乎是 Delphi 中的回归。

在 Delphi 2010 中的输出是“奇怪的”。但在 XE2 中没有输出,因此该错误不存在。我手头没有 XE 可供测试,但感谢@Sertac 确认 XE 也输出“奇怪”。请注意,旧版本的 Delphi 也可以,所以这是 2009 年左右的回归。

在 2010 年生成的代码是:

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');

文字 1.32 存储为一个 10 字节的浮点值,其值应为 13200。这是一个可精确表示的二进制浮点值。存储为 10 字节浮点数的 13200 的位模式为:

00 00 00 00 00 00 40 CE 0C 40

但是,存储在 $00405118 的文字中的位模式不同,并且略大于 13200。值为:

01 00 00 00 00 00 40 CE 0C 40

这就解释了为什么C &lt; 1.32 的计算结果为True

在 XE2 上生成的代码是:

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');

注意这里的文字被保存在一个 4 字节的浮点数中。这可以从我们与dword ptr [$00406128] 进行比较的事实中看出。如果我们查看存储在$00406128 的单精度浮点数的内容,我们会发现:

00 40 4E 46

这正是 13200 表示为 4 字节浮点数。

我的猜测是,2010 年的编译器在遇到1.32 时会执行以下操作:

  • 将 1.32 转换为最接近的可精确表示的 10 字节浮点数。
  • 将该值乘以 10000。
  • 将生成的 10 字节浮点数存储在 $00405118

因为 1.32 不能精确表示,所以最终的 10 字节浮点数并不完全是 13200。并且推测当编译器从将这些文字存储在 4 字节浮点数中切换到将它们存储在 10 字节浮点数中时,就会出现回归。

根本问题是德尔福对Currency 数据类型的支持是建立在一个完全有缺陷的设计之上的。使用二进制浮点运算来实现十进制定点数据类型简直是自找麻烦。修复设计的唯一合理方法是完全重新设计编译器以使用定点整数运算。令人失望的是,新的 64 位编译器使用与 32 位编译器相同的设计。

老实说,我会阻止 Delphi 编译器对 Currency 文字进行任何浮点运算。这只是一个完整的雷区。我会像这样在脑海中进行 10,000 次转换:

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

然后调用代码将是:

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

编译器无法解决这个问题!

哼!

【讨论】:

  • 我不明白为什么fldfildfcompp 是错误代码。
  • 实际上“正确”代码将货币与单个 (dword ptr) 进行比较,而“错误”代码与扩展 (tbyte ptr) 进行比较。这就是区别,不是fld
  • @Serg 不,根本不是这样。您观察到的是,一个版本将文字存储为 Single,而另一个版本将其存储为 Extended。但无论哪种方式,值都是13200
  • @Serg 你知道为什么 Currency 使用浮点硬件单元对 64 位整数进行操作吗?
  • @Serg 不过,你确实让我走上了正轨。事实证明,Delphi 编译器无法将 1.32 转换为 13200。还不知道为什么。
【解决方案2】:

像 Currency(1.32) 这样的硬转换是不可能的,您可以使用以下内容进行显式转换

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;

另一种方法是通过使用常量或变量来强制使用货币

const
  comp:Currency=1.32;
var
  C: Currency;
begin
  C := 1.32;
  if C < comp then
  begin
    writeln ('strange');
  end;
end;

【讨论】:

  • @DavidHeffernan 我同意,但 fld 不与上述两种代码变体一起使用。
  • +1 并感谢您提供此(现已接受)解决方案:恕我直言,使用 const 是解决这个非常讨厌的错误的最简单方法
  • 使用类型化常量是一种好方法,因为它强制编译器生成整数 13200 而不是浮点值
【解决方案3】:

为了避免这个问题(编译器中的错误),您可以按照@bummi 的建议进行操作, 或试试这个运行时演员表:

if C < Currency(Variant(1.32)) then

为避免往返 FPU(和舍入误差),请考虑使用此比较函数:

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;

有关更多信息,请参阅此页面,Floating point and Currency fields

【讨论】:

    【解决方案4】:

    补充大卫的答案 - 下一个代码并不奇怪,虽然相当于 OP 代码:

    program Project2;
    
    {$APPTYPE CONSOLE}
    
    var
      I: Int64;
      E: Extended;
    
    begin
      I:= 13200;
      E:= 13200;
      if I < E then
      begin
        WriteLn('strange');
      end;
      ReadLn;
    end.
    

    现在编译器会为 Extended(13200) 生成正确的二进制值,所以问题似乎与 Delphi 编译器中的错误 Currency 类型实现有关。

    【讨论】:

    • 这是因为编译器要计算1.32*10000。但是在您的代码中,您输入了预先计算的 13200。
    • 在 OP 代码中,编译器也使用预计算值(Delphi XE),但预计算错误。
    • 这是因为编译器使用二进制表示计算,而我们使用十进制表示。
    • @DavidHeffernan 这很可能的解释再次指出了错误的Currency 字体设计。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-03-10
    • 2014-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多