Как оптимизировать DivMod для постоянного делителя 10 - PullRequest
0 голосов
/ 29 октября 2018

В модуле Delphi math.pas есть процедура DivMod , которую я хочу преобразовать в линейный и оптимизировать, чтобы делитель всегда был равен 10. Но я не знаю деталей Пентагона ASM. Что такое конвертация сильфонной процедуры

 procedure DivMod(Dividend: Integer; Divisor: Word;
  var Result, Remainder: Word);
asm
        PUSH    EBX
        MOV     EBX,EDX
        MOV     EDX,EAX
        SHR     EDX,16
        DIV     BX
        MOV     EBX,Remainder
        MOV     [ECX],AX
        MOV     [EBX],DX
        POP     EBX
end;

Ответы [ 4 ]

0 голосов
/ 29 октября 2018

Исходя из этого ответа , вы можете получить некоторые производительности обратно в 32-битной компиляции, используя аппаратное 32x32 -> 64-битное умножение с использованием SSE:

program Project1;
{$APPTYPE CONSOLE}

uses
  Windows, SysUtils;

procedure DivMod10(num : Cardinal; var q, r : Cardinal);
const
  m : cardinal = 3435973837;
asm
  movd xmm0, m         {move magic number to xmm0}
  movd xmm1, eax       {move num to xmm1}
  pmuludq xmm0, xmm1   {xmm0[0:32] * xmm1[0:32] -> xmm0[0:64] unsigned}
  psrlq xmm0, 35       {right shift xmm0}
  movss [edx], xmm0    {store quotient to q}
  movd edx, xmm0       {recycle edx, store q}
  imul edx, -$A        {edx = q * (-10)}
  add edx, eax         {edx = r}
  mov [ecx], edx       {store r}
end;

var
  q, r, t0, i : cardinal;
begin
  t0 := GetTickCount;
  for I := 1 to 999999999 do DivMod10(i, q, r);
  WriteLn('SSE ASM : ' + IntToStr(GetTickCount - t0));

  t0 := GetTickCount;
  for I := 1 to 999999999 do q := i div 10;
  WriteLn('div : ' + IntToStr(GetTickCount - t0));

  WriteLn('Test correctness...');
  for I := 1 to High(Cardinal) do begin
    DivMod10(i,q,r);
    if (q <> (i div 10)) or (r <> (i mod 10)) then
      WriteLn('Incorrect Result : ' + IntToStr(i));
  end;

  WriteLn('Test complete.');
  Readln;
end.

Это производит:

SSE ASM: 2449
div: 3401
Проверка правильности ...
Тест завершен.

Это не , как правило, безопасно, так как вы должны проверять во время выполнения, поддерживает ли ЦП необходимые инструкции SSE (и иметь альтернативу purepascal для этого случая), тем не менее, все реже находить ЦП живы и работают достаточно взрослые, чтобы не поддерживать хотя бы SSE2.

Для систем, которые его поддерживают, это может быть более производительным, чем div (я вижу около 25% выигрыша в производительности при использовании, например, DivMod10 в Haswell), и вы получите остаток. Не так быстро, как родной 64-битный IMUL, но все же весьма полезен.


Чтобы обратиться к комментариям Питера, рассмотрим версию для x86:

procedure DivMod10(num : Cardinal; var q, r : Cardinal);
const
  m : cardinal = 3435973837;
asm
  push eax
  push edx
  mul m
  mov eax, edx
  shr eax, 3
  pop edx
  mov [edx], eax
  pop eax
  imul edx, [edx], -$A
  add edx, eax
  mov [ecx], edx
end;

, который производит (для меня - Haswell i7):

x86 ASM: 2948
div: 3401
Проверка правильности ...
Тест завершен.

Что примерно на 18% медленнее, чем версия SSE.


С некоторыми хорошими идеями от Питера, мы можем немного оптимизировать версию x86 для чистого компьютера, сохранив регистр путем преобразования в функцию и заменив непосредственные imul на lea и add:

function DivMod10(Num: Cardinal; var Quotient: Cardinal): Cardinal;
const
  m : cardinal = 3435973837;
asm
  mov ecx, eax           {save num to ecx}
  push edx               {save quotient pointer}
  mul m                  {edx:eax = m*Num}
  shr edx, 3             {edx = quotient}
  pop eax                {restore quotient pointer}
  mov [eax], edx         {store quotient}
  mov eax, ecx           {restore num to eax}
  lea ecx, [edx +4*edx]  {ecx = q*5}
  add ecx, ecx           {ecx = q*10}
  sub eax, ecx           {return remainder in eax}
end;

Время выполнения (при тех же условиях, что и выше) уменьшается до 2637ms, но все же не так быстро, как в версии SSE. Оптимизация от imul до lea незначительна и оптимизирует задержку по сравнению с пропускной способностью - это может применяться ко всем алгоритмам в зависимости от среды конечного использования.

0 голосов
/ 29 октября 2018

Наиболее важной оптимизацией, которую вы можете сделать, является использование мультипликативного обратного с фиксированной запятой для деления на константу времени компиляции: Почему GCC использует умножение на странное число при реализации целочисленного деления? .

Любой приличный компилятор C сделает это за вас, но, очевидно, Delphi этого не сделает, поэтому есть веская причина сделать это с помощью asm.

Можете ли вы вернуть значение в EAX вместо того, чтобы хранить как частное, так и остаток в памяти? Похоже на пустую трата времени, чтобы передать 2 аргумента указателя и заставить вызывающего извлекать значение из памяти. (Обновите, да, я думаю, вы можете сделать это функцией, а не процедурой; я просто слепо модифицирую код Delphi из других ответов.)

В любом случае, к счастью, мы можем заставить компилятор C выполнить тяжелую работу по выяснению мультипликативного обратного и подсчета сдвига за нас. Мы даже можем заставить его использовать то же «соглашение о вызовах», которое выглядит так, как будто Delphi использует для встроенного asm. regparm=3 32-битное соглашение о вызовах GCC передает аргументы в EAX, EDX и ECX (в этом порядке).

Возможно, вы захотите создать отдельную версию для случаев, когда вам нужно только частное, потому что (в отличие от медленной инструкции div), вы должны вычислить остаток отдельно как x - (x/y)*y, если вы используете быстрый мультипликативный обратное. Но да, это все еще примерно в два раза быстрее, чем в современной x86.

Или же вы можете оставить вычисление остатка в чистом Delphi, если компилятор просто ужасен при оптимизации в целом.

#ifdef _MSC_VER
#define CONVENTION  _fastcall   // not the same, but 2 register args are better than none.
#else
#define CONVENTION __attribute__((regparm(3)))
#endif

// use gcc -Os to get it to emit code with actual div.

divmod10(unsigned x, unsigned *quot, unsigned *rem) {
    unsigned tmp = x/10;
    // *quot = tmp;
    *rem = x%10;
    return tmp;
}

Из проводника компилятора Godbolt :

# gcc8.2  -O3 -Wall -m32
div10:    # simplified version without the remainder, returns in EAX
        mov     edx, -858993459     # 0xCCCCCCCD
        mul     edx                 # EDX:EAX = dividend * 0xCCCCCCCD
        mov     eax, edx
        shr     eax, 3
        ret
      # quotient in EAX

# returns quotient in EAX, stores remainder to [ECX]
# quotient pointer in EDX is unused (and destroyed).
divmod10:
        mov     edx, -858993459
        push    ebx
        mov     ebx, eax
        mul     edx                      # EDX:EAX = dividend * 0xCCCCCCCD
        mov     eax, edx
        shr     eax, 3
        # quotient in EAX = high_half(product) >> 3 = product >> (32+3)
        lea     edx, [eax+eax*4]         # EDX = quotient*5
        add     edx, edx                 # EDX = quot * 10
        sub     ebx, edx                 # remainder = dividend - quot*10
        mov     DWORD PTR [ecx], ebx     # store remainder
        pop     ebx
        ret
        # quotient in EAX

Это вывод компилятора C. При необходимости адаптируйтесь к Delphi inline asm; входы в правильные регистры для Delphi, я думаю .

Если Delphi inline-asm не позволяет вам забить EDX, вы можете сохранить / восстановить его. Или вы хотите удалить неиспользуемый ввод указателя quotient, затем вы можете настроить asm или настроить C на Godbolt и посмотреть на вывод нового компилятора.

Это больше инструкций, чем с div, но div очень медленный (10 моп, и задержка 26 циклов даже на Skylake.)

Если у вас есть 64-битный целочисленный тип в Delphi, вы можете сделать это в Delphi source и избежать встроенного asm. Или, как показывает MBo, вы можете использовать $CCCD в качестве мультипликативного обратного для входов, которые находятся в диапазоне 0..2 ^ 16-1, используя только 32-битные целочисленные типы.

В остальном, циклическое сохранение / перезагрузка (от 4 до 5 циклов) имеет задержку, аналогичную фактическому вычислению на недавнем процессоре Intel с mov-elmination (3 + 1 к частному, + еще 3 для lea / add / sub = 7), поэтому использовать встроенный asm для этого довольно дерьмо. Но это все же лучше, чем div инструкция для задержки и пропускной способности. См. https://agner.org/optimize/ и другие ссылки на производительность в теге x86 вики .


Delphi версия, которую вы можете скопировать / вставить

( Если я понял это правильно, я не знаю Delphi, и просто скопировал + измененные примеры здесь на SO и этот сайт , основываясь на том, что я сделал вывод о соглашении о вызовах / синтаксис )

Я не уверен, что получил правильный аргумент для inline-asm. Эта документация RADStudio гласит: «За исключением оператора ESP и EBP, оператор asm может ничего не предполагать в отношении содержимого регистра при входе в оператор». Но я предполагаю, что аргументы в EAX и EDX.

Использование asm для 64-битного кода может быть глупым, потому что в 64-битной системе вы можете эффективно использовать чистый Паскаль для 64-битного умножения. Как реализовать эффективный 32-битный DivMod в 64-битном коде . Таким образом, в блоках {$IFDEF CPUX64} лучшим выбором может быть чистый паскаль с использованием UInt64(3435973837)*num;

function Div10(Num: Cardinal): Cardinal;
{$IFDEF PUREPASCAL}
begin
  Result := Num div 10;
end;
{$ELSE !PUREPASCAL}
{$IFDEF CPUX86}
asm
        MOV     EDX, $CCCCCCCD
        MUL     EDX                   // EDX:EAX = Num * fixed-point inverse
        MOV     EAX,EDX               // mov then overwrite is ideal for Intel mov-elimination
        SHR     EAX,3
end;
{$ENDIF CPUX86}
{$IFDEF CPUX64}
asm
         // TODO: use pure pascal for this; Uint64 is efficient on x86-64
        // Num in ECX, upper bits of RCX possibly contain garbage?
        mov     eax, ecx              // zero extend Num into RAX
        mov     ecx, $CCCCCCCD        // doesn't quite fit in a sign-extended 32-bit immediate for imul
        imul    rax, rcx              // RAX = Num * fixed-point inverse
        shr     rax, 35               // quotient = eax
end;
{$ENDIF CPUX64}
{$ENDIF}

 {Remainder is the function return value}
function DivMod10(Num: Cardinal; var Quotient: Cardinal): Cardinal;
{$IFDEF PUREPASCAL}
begin
  Quotient := Num div 10;
  Result := Num mod 10;
end;
{$ELSE !PUREPASCAL}
{$IFDEF CPUX86}
asm
    // Num in EAX,  @Quotient in EDX
    push    esi
    mov     ecx, edx           // save @quotient
    mov     edx, $CCCCCCCD
    mov     esi, eax           // save dividend for use in remainder calc
    mul     edx                // EDX:EAX = dividend * 0xCCCCCCCD
    shr     edx, 3             // EDX = quotient
    mov     [ecx], edx         // store quotient into @quotient

    lea     edx, [edx + 4*edx] // EDX = quot * 5
    add     edx, edx           // EDX = quot * 10
    mov     eax, esi                  // off the critical path
    sub     eax, edx           // Num - (Num/10)*10
    pop     esi
    // Remainder in EAX = return value
end;
{$ENDIF CPUX86}
{$IFDEF CPUX64}
asm
        // TODO: use pure pascal for this?  Uint64 is efficient on x86-64
    // Num in ECX,   @Quotient in RDX
    mov     r8d, ecx          // zero-extend Num into R8
    mov     eax, $CCCCCCCD
    imul    rax, r8
    shr     rax, 35           // quotient in eax

    lea     ecx, [rax + 4*rax]
    add     ecx, ecx          // ecx = 10*(Num/10)
    mov     [rdx], eax        // store quotient

    mov     eax, r8d          // copy Num again
    sub     eax, ecx          // remainder = Num - 10*(Num/10)
    // we could have saved 1 mov instruction by returning the quotient
    // and storing the remainder.  But this balances latency better.
end;
{$ENDIF CPUX64}
{$ENDIF}

Сохранение фактора и возврат остатка означает, что оба могут быть готовы примерно в одно и то же время в вызывающей стороне, потому что дополнительная задержка вычисления остатка от коэффициента перекрывается с пересылкой хранилища. IDK, если это хорошо, или если выполнение не по порядку выполнено, начните с какой-то работы, основанной на частном, чаще лучше Я догадываюсь, что если вы позвоните в DivMod10, вам может понадобиться только остаток.

Но в цикле разбиения на десятичные цифры, который многократно делится на 10, это частное, которое формирует критический путь, поэтому версия этого, которая возвращала бы частное и сохраняла остаток, была бы намного лучше Выбор есть.

В этом случае вы сделаете частное значение возвращаемым в EAX и переименуете функцию arg в остаток.


Asm основан на выводе clang для этой версии этой функции C (https://godbolt.org/z/qu2kvV),, предназначенной для соглашения о вызовах Windows x64. Но с некоторыми настройками, чтобы сделать его более эффективным, например, убрав mov с критического пути и использование разных регистров, чтобы избежать префиксов REX. И замена одного LEA просто ADD.

unsigned divmod10(unsigned x, unsigned *quot) {
    unsigned qtmp = x/10;
    unsigned rtmp = x%10;
     *quot = qtmp;
     //*rem = rtmp;
    return rtmp;
}

Я использовал версию clang вместо gcc, потому что imul r64,r64 быстрее на процессорах Intel и Ryzen (3 цикла задержки / 1 моп). mul r32 - это 3 мопа, и только 1 на 2 такта в семействе Sandybridge. Я думаю, что аппаратное умножение естественным образом дает 128-битный результат, и разбиение младших 64 из них на edx: eax требует дополнительного uop или чего-то в этом роде.

0 голосов
/ 29 октября 2018

Хорошо, вот моя попытка:

procedure DivMod10(Num: Cardinal; var Quotient, Remainder: Cardinal);
asm
        PUSH    ESI
        PUSH    EDI
        MOV     EDI,EAX          // Num
        MOV     ESI,EDX          // @Quotient
        MOV     EDX,$CCCCCCCD    
        MUL     EDX              // EDX:EAX = EAX * magic_number
        SHR     EDX,3
        MOV     [ESI],EDX        // --> @Quotient
        LEA     EDX,[EDX+4*EDX]
        ADD     EDX,EDX          // Quotient * 10 
        SUB     EDI,EDX          // Num - Quotient*10
        MOV     [ECX],EDI        // --> @Remainder 
        POP     EDI
        POP     ESI
end;

Если вам не нужен остаток:

function Div10(Num: Cardinal): Cardinal;
asm
        MOV     ECX,$CCCCCCCD    
        MUL     ECX   
        SHR     EDX,3
        MOV     EAX,EDX
end;
0 голосов
/ 29 октября 2018

Прибыль существует, но значима ли она для реальных задач? (обратите внимание, я изменил типы аргументов)

procedure DivMod10(Dividend: DWord; var Result, Remainder: DWord);
asm
        PUSH    EDI
        PUSH    ESI
        MOV     EDI, EDX
        MOV     ESI, 10
        XOR     EDX, EDX
        DIV     ESI
        MOV     [ECX], EDX
        MOV     [EDI], EAX
        POP     ESI
        POP     EDI
end;

  1 000 000 000 iterations
  divmod10: 4539
  math.divmod: 7145

Самый быстрый способ для ограниченного диапазона - Код Delphi с использованием умножения, как предложено @Peter Cordes. Код сборки медленнее (1777), возможно, из-за вызова функции и моего слабого опыта сборки.

  b := a * $CCCD;
  b := b shr 19;   //result
  c := a - b * 10;  //remainder
  1 000 000 000 iterations: 1200 ms  (560 ms without remainder)

Использование константы из этого ответа SO позволяет избавиться от смены, но время хуже, чем у версий Петра и J:

function DM10(Dividend: DWord;  var Remainder: DWord): DWord;
asm
   push ebx
   mov ebx, eax
   mov ecx, edx
   mov edx, 1999999Ah
   mul eax, edx
   push edx
   lea eax, [edx+edx*4]
   add eax, eax
   sub ebx, eax
   mov [ecx], ebx
   pop eax
   pop ebx
end;

Timings for my machine (10^9 iterations, haswell i5-4670):
this  DM10               2013
Peter Cordes DivMod10    1755
J... SSE version         1685
...