Что, кроме опций «Компиляции», может изменить поколение кода в 64-битной версии?

#delphi #delphi-10-seattle

#delphi #delphi-10-Сиэтл

Вопрос:

Введение

Я столкнулся с проблемой с Currency в одном из наших приложений. Я получал разные результаты в Win32 и Win64. Я нашел здесь статью, в которой показана аналогичная проблема, но она была исправлена в XE6. Первое, что я попытался сделать, это создать MCVE для дублирования проблемы. Вот тут-то и отвалились колесики. То, что выглядит как идентичный код в MCVE, приводит к другому результату по сравнению с приложением. Сгенерированный 64-битный код отличается. Итак, мой вопрос превратился в то, почему они разные, и как только я это выясню, я смогу создать подходящий MCVE.

У меня есть метод, который вычисляет общее количество. Этот метод вызывает другой метод для получения значения, которое необходимо добавить к итогу. Метод возвращает единицу. Я присваиваю переменной единственное значение, а затем добавляю его к сумме, которая является валютой. В моем основном приложении значение для total используется позже, но добавление его в MCVE не меняет поведение. Я убедился, что параметры компилятора были одинаковыми.

В моем основном приложении результат вычисления составляет 2469.6001 долларов США в Win32 и 2469.6 долларов США в Win64, но я не могу дублировать это в MCVE. Все на странице параметров компиляции было таким же, а оптимизация была отключена.

Попытка MCVE

Вот код для моей попытки MCVE. Это имитирует действия в исходном приложении.

 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.
  

Этот код генерирует следующий ассемблер для последних двух строк TTestClass.Вычислить:

 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
  

Основное приложение

Это выдержка из основного приложения. Сложно предоставить больше информации, но я не думаю, что это изменит суть вопроса. В этом классе FBulkTotal объявлен как валюта, которая является строго приватной. UpdateTotals является общедоступным.

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

Сгенерированный код для этих двух строк является:

 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
  

Странно то, что сгенерированный код отличается. MCVE имеет cvtsi2sd, за которым следует cvtss2sd, но это основное приложение использует movss вместо cvtss2sd при копировании содержимого единственного значения в регистр xmm1. Я почти уверен, что именно это вызывает другой результат, но, не имея возможности создать MCVE, я даже не могу подтвердить, что это проблема с компилятором.

Вопрос

Мой вопрос в том, что может вызвать эти различия в генерации кода? Я предполагал, что оптимизации могут выполнять подобные действия, но я убедился, что они одинаковы.

Комментарии:

1. Win32 использует FPU и внутренне 80-разрядный расширенный тип. Win64 использует SSE2, и у него нет такого расширенного типа для внутренних вычислений, поэтому результаты могут немного отличаться. Я не могу это протестировать (здесь нет Win64), но я предполагаю, что это причина проблемы. Currency — это просто масштабированный Int64, но смешивание его с Single и / или Double может привести к несколько иным результатам. Если вы действительно хотите точности, используйте мой тип BigDecimal повсюду.

2. Currency — несколько дерьмовый тип. Лучше использовать правильный десятичный тип.

3. Хотя эта ситуация с созданием кода представляет собой непреодолимую загадку, смешивание типов currency и с плавающей запятой — отличный способ снизить точность, которую вы пытаетесь сохранить, используя тип currency, даже когда все работает так, как задумано. Зачем вы это делаете в первую очередь?

4. Обратите внимание, что если вы явно помещаете значения fp в переменные currency перед выполнением операций с другими значениями currency, компилятор всегда поступит правильно (т. Е.: cvtss2sd, разделите на 10000, cvtsd2si, затем добавьте значения currency в виде целых чисел). В обоих приведенных выше случаях похоже, что компилятор выполняет разные, но оба неправильные действия — преобразует и добавляет значения либо в одиночном, либо в двойном виде, прежде чем приводить обратно к целому числу. Является ли это ошибкой или Emba просто не продумала, как они хотели, чтобы этот тип работал, я согласен с @DavidHeffernan в том, что это неправильный тип данных.

Ответ №1:

Вы не должны использовать какие-либо значения типа с плавающей запятой при работе с валютой.

Я рекомендую вам посмотреть видео с числами с плавающей запятой от Computerphile, где он объясняет, как значения с плавающей запятой обрабатываются компьютерами и почему их не следует использовать при обработке валюты. https://www.youtube.com/watch?v=PZRI1IfStY0