Почему 64-битное приложение Delphi вычисляет разные результаты, чем 32-битная сборка?

Недавно мы начали процесс создания 64-битных сборников наших приложений. Во время тестирования сравнения мы обнаружили, что 64-битная сборка вычисляется по-разному. У меня есть образец кода, который демонстрирует разницу между двумя сборками.

var
  currPercent, currGross, currCalcValue : Currency;
begin

  currGross := 1182.42;
  currPercent := 1.45;
  currCalcValue := (currGross * (currPercent * StrToCurr('.01')));
  ShowMessage(CurrToStr(currCalcValue));
end;

Если вы выполните это в 32-битной версии, currCalcValue вычисляется с 17.1451, а 64-разрядная версия возвращается с 17.145.

Почему не 64-битная сборка, вычисляющая дополнительное десятичное место? Все переменные определяются как 4 значения десятичной суммы.

Ответы

Ответ 1

Здесь мой SSCCE основан на вашем коде. Обратите внимание на использование консольного приложения. Делает жизнь намного проще.

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  currPercent, currGross, currCalcValue : Currency;
begin
  currGross := 1182.42;
  currPercent := 1.45;
  currCalcValue := (currGross * (currPercent * StrToCurr('.01')));
  Writeln(CurrToStr(currCalcValue));
  Readln;
end.

Теперь посмотрите на код, который сгенерирован. Первые 32 бит:

Project3.dpr.13: currCalcValue := (currGross * (currPercent * StrToCurr('.01')));
0041C409 8D45EC           lea eax,[ebp-$14]
0041C40C BADCC44100       mov edx,$0041c4dc
0041C411 E8A6A2FEFF       call @UStrLAsg
0041C416 8B1504E74100     mov edx,[$0041e704]
0041C41C 8B45EC           mov eax,[ebp-$14]
0041C41F E870AFFFFF       call StrToCurr
0041C424 DF7DE0           fistp qword ptr [ebp-$20]
0041C427 9B               wait 
0041C428 DF2DD83E4200     fild qword ptr [$00423ed8]
0041C42E DF6DE0           fild qword ptr [ebp-$20]
0041C431 DEC9             fmulp st(1)
0041C433 DF2DE03E4200     fild qword ptr [$00423ee0]
0041C439 DEC9             fmulp st(1)
0041C43B D835E4C44100     fdiv dword ptr [$0041c4e4]
0041C441 DF3DE83E4200     fistp qword ptr [$00423ee8]
0041C447 9B               wait 

И 64 бит:

Project3.dpr.13: currCalcValue := (currGross * (currPercent * StrToCurr('.01')));
0000000000428A0E 488D4D38         lea rcx,[rbp+$38]
0000000000428A12 488D1513010000   lea rdx,[rel $00000113]
0000000000428A19 E84213FEFF       call @UStrLAsg
0000000000428A1E 488B4D38         mov rcx,[rbp+$38]
0000000000428A22 488B155F480000   mov rdx,[rel $0000485f]
0000000000428A29 E83280FFFF       call StrToCurr
0000000000428A2E 4889C1           mov rcx,rax
0000000000428A31 488B0510E80000   mov rax,[rel $0000e810]
0000000000428A38 48F7E9           imul rcx
0000000000428A3B C7C110270000     mov ecx,$00002710
0000000000428A41 48F7F9           idiv rcx
0000000000428A44 488BC8           mov rcx,rax
0000000000428A47 488B0502E80000   mov rax,[rel $0000e802]
0000000000428A4E 48F7E9           imul rcx
0000000000428A51 C7C110270000     mov ecx,$00002710
0000000000428A57 48F7F9           idiv rcx
0000000000428A5A 488905F7E70000   mov [rel $0000e7f7],rax

Обратите внимание, что 32-битный код выполняет арифметику на FPU, но 64-разрядный код выполняет его с использованием целочисленной арифметики. Это ключевое различие.

В 32-битном коде выполняется следующий расчет:

  • Преобразовать '0.01' в валюту, которая равна 100, что позволяет сдвиг с фиксированной точкой в ​​10 000.
  • Загрузите 14 500 в FPU.
  • Умножьте на 100, давая 1,450,000.
  • Умножьте на 11 824 200, получив 17 145 090 000 000.
  • Разделите на 10 000 ^ 2, давая 171 450.
  • округление до ближайшего целого числа, дающее 171 451.
  • Сохраните это в своей валютной переменной. Следовательно, результат равен 17.1451.

Теперь, в 64-битном коде, это немного отличается. Потому что мы используем 64-битные целые числа. Это выглядит так:

  • Конвертировать '0.01' в валюту, которая равна 100.
  • Умножьте на 14 500, что составляет 1 450 000.
  • Разделить на 10 000, что составляет 145.
  • Умножьте на 11 824 200, давая 1 714 509 000.
  • Разделить на 10 000, что составляет 171 450 человек. Ух-о, потеря точности здесь.
  • Сохраните это в своей валютной переменной. Следовательно, результат равен 17.145.

Таким образом, проблема заключается в том, что 64-битный компилятор делит на 10 000 на каждом промежуточном этапе. Предположительно, чтобы избежать переполнения, гораздо более вероятно в 64-битном целочисленном, чем регистр с плавающей запятой.

Если бы это было сделано так:

100 * 14,500 * 11,824,200 / 10,000 / 10,000

он получит правильный ответ.

Ответ 2

Это исправлено с XE5u2 и с текущей записью XE6u1.