Самый быстрый способ вычисления 128-битного целого по модулю 64-битного целого - PullRequest
53 голосов
/ 02 апреля 2010

У меня 128-разрядное целое число без знака A и 64-разрядное целое число без знака B. Какой самый быстрый способ вычисления A % B - это (64-разрядное) остаток от деления A на B?

Я хочу сделать это на языке C или ассемблере, но мне нужно ориентироваться на 32-битную платформу x86. К сожалению, это означает, что я не могу воспользоваться преимуществами поддержки компилятора для 128-разрядных целых чисел или способности архитектуры x64 выполнять требуемую операцию в одной инструкции.

Edit:

Спасибо за ответы до сих пор. Тем не менее, мне кажется, что предлагаемые алгоритмы будут довольно медленными - разве самый быстрый способ выполнить 128-разрядное 64-разрядное деление состоит в том, чтобы использовать встроенную поддержку процессора для 64-разрядного 32-разрядного деления? Кто-нибудь знает, есть ли способ выполнить большее деление с помощью нескольких меньших делений?

Re: Как часто меняется B?

Прежде всего меня интересует общее решение - какие вычисления вы бы выполнили, если бы А и В каждый раз отличались?

Однако вторая возможная ситуация заключается в том, что B не так часто меняется, как A - их может быть целых 200, чтобы делить на B. Как ваш ответ будет отличаться в этом случае?

Ответы [ 13 ]

32 голосов
/ 02 апреля 2010

Вы можете использовать версию деления Русское Крестьянское Умножение .

Чтобы найти остаток, выполните (в псевдокоде):

X = B;

while (X <= A/2)
{
    X <<= 1;
}

while (A >= B)
{
    if (A >= X)
        A -= X;
    X >>= 1;
}

Модуль оставлен в А.

Вам потребуется реализовать сдвиги, сравнения и вычитания для работы со значениями, состоящими из пары 64-разрядных чисел, но это довольно тривиально.

Это будет цикл не более 255 раз (со 128-битным A). Конечно, вам необходимо выполнить предварительную проверку делителя нуля.

13 голосов
/ 06 апреля 2010

Возможно, вы ищете готовую программу, но основные алгоритмы арифметики с множественной точностью можно найти в Искусство компьютерного программирования Кнута , том 2. Вы можете найти алгоритм деления, описанный онлайн здесь . Алгоритмы имеют дело с произвольной арифметикой с множественной точностью и поэтому являются более общими, чем вам нужно, но вы должны быть в состоянии упростить их для 128-битной арифметики, выполненной на 64- или 32-битных цифрах. Будьте готовы к разумной работе (а) понимая алгоритм и (б) преобразовав его в C или ассемблер.

Возможно, вы также захотите проверить Восторг хакера , который полон очень умных ассемблеров и других низкоуровневых хакеров, включая некоторые арифметики с высокой точностью.

11 голосов
/ 05 апреля 2010

Дано A = AH*2^64 + AL:

A % B == (((AH % B) * (2^64 % B)) + (AL % B)) % B
      == (((AH % B) * ((2^64 - B) % B)) + (AL % B)) % B

Если ваш компилятор поддерживает 64-битные целые числа, то это, вероятно, самый простой способ. Внедрение MSVC 64-битного модуля по 32-битной x86 представляет собой сборку, заполненную волосатыми циклами (VC\crt\src\intel\llrem.asm для смелых), так что я бы с этим согласился.

8 голосов
/ 06 апреля 2010

Это практически непроверенная, частично измененная по скорости, функция алгоритма 'русского крестьянина' Mod128by64. К сожалению, я пользователь Delphi, поэтому эта функция работает под Delphi. :) Но ассемблер почти такой же, так что ...

function Mod128by64(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
//   : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//Divisor = edx:ebp
//Dividend = bh:ebx:edx //We need 64 bits + 1 bit in bh
//Result = esi:edi
//ecx = Loop counter and Dividend index
  push    ebx                     //Store registers to stack
  push    esi
  push    edi
  push    ebp
  mov     ebp, [edx]              //Divisor = edx:ebp
  mov     edx, [edx + 4]
  mov     ecx, ebp                //Div by 0 test
  or      ecx, edx                
  jz      @DivByZero
  xor     edi, edi                //Clear result
  xor     esi, esi
//Start of 64 bit division Loop
  mov     ecx, 15                 //Load byte loop shift counter and Dividend index
@SkipShift8Bits:                  //Small Dividend numbers shift optimisation
  cmp     [eax + ecx], ch         //Zero test
  jnz     @EndSkipShiftDividend
  loop    @SkipShift8Bits         //Skip 8 bit loop
@EndSkipShiftDividend:
  test    edx, $FF000000          //Huge Divisor Numbers Shift Optimisation
  jz      @Shift8Bits             //This Divisor is > $00FFFFFF:FFFFFFFF
  mov     ecx, 8                  //Load byte shift counter
  mov     esi, [eax + 12]         //Do fast 56 bit (7 bytes) shift...
  shr     esi, cl                 //esi = $00XXXXXX
  mov     edi, [eax + 9]          //Load for one byte right shifted 32 bit value
@Shift8Bits:
  mov     bl, [eax + ecx]         //Load 8 bits of Dividend
//Here we can unrole partial loop 8 bit division to increase execution speed...
  mov     ch, 8                   //Set partial byte counter value
@Do65BitsShift:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  setc    bh                      //Save 65th bit
  sub     edi, ebp                //Compare dividend and  divisor
  sbb     esi, edx                //Subtract the divisor
  sbb     bh, 0                   //Use 65th bit in bh
  jnc     @NoCarryAtCmp           //Test...
  add     edi, ebp                //Return privius dividend state
  adc     esi, edx
@NoCarryAtCmp:
  dec     ch                      //Decrement counter
  jnz     @Do65BitsShift
//End of 8 bit (byte) partial division loop
  dec     cl                      //Decrement byte loop shift counter
  jns     @Shift8Bits             //Last jump at cl = 0!!!
//End of 64 bit division loop
  mov     eax, edi                //Load result to eax:edx
  mov     edx, esi
@RestoreRegisters:
  pop     ebp                     //Restore Registers
  pop     edi
  pop     esi
  pop     ebx
  ret
@DivByZero:
  xor     eax, eax                //Here you can raise Div by 0 exception, now function only return 0.
  xor     edx, edx
  jmp     @RestoreRegisters
end;

Возможна хотя бы еще одна оптимизация скорости! После «Оптимизации смещения огромных чисел делителей» мы можем проверить старший бит делителей, если он равен 0, нам не нужно использовать дополнительный регистр bh как 65-й бит для хранения в нем. Так что развернутая часть цикла может выглядеть так:

  shl     bl,1                    //Shift dividend left for one bit
  rcl     edi,1
  rcl     esi,1
  sub     edi, ebp                //Compare dividend and  divisor
  sbb     esi, edx                //Subtract the divisor
  jnc     @NoCarryAtCmpX
  add     edi, ebp                //Return privius dividend state
  adc     esi, edx
@NoCarryAtCmpX:
4 голосов
/ 11 апреля 2010

Я хотел бы поделиться несколькими мыслями.

Боюсь, не все так просто, как предлагает MSN.

В выражении:

(((AH % B) * ((2^64 - B) % B)) + (AL % B)) % B

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

Мне было любопытно, как в MSVC реализована 64-битная операция по модулю, и я попытался что-то выяснить. Я на самом деле не знаю ассемблера, и все, что у меня было доступно, это Express Edition, без источника VC \ crt \ src \ intel \ llrem.asm, но я думаю, что мне удалось получить представление о том, что происходит, после небольшой игры с выводом отладчика и разборки. Я попытался выяснить, как вычисляется остаток в случае натуральных чисел и делителя> = 2 ^ 32. Конечно, есть некоторый код, который работает с отрицательными числами, но я не стал копаться в этом.

Вот как я это вижу:

Если делитель> = 2 ^ 32, то и делитель, и делитель сдвигаются вправо настолько, насколько это необходимо, чтобы разделитель делился на 32 бита. Другими словами: если для записи делителя в двоичном виде требуется n цифр и n> 32, то n-32 младших разрядов как делителя, так и дивиденда отбрасываются. После этого деление выполняется с использованием аппаратной поддержки для деления 64-битных целых чисел на 32-битные. Результат может быть неверным, но я думаю, что можно доказать, что результат может быть не более чем на 1. После деления делитель (исходный) умножается на результат, а произведение вычитается из дивиденда. Затем это исправляется добавлением или вычитанием делителя, если необходимо (если результат деления был на единицу).

Легко разделить 128-битное целое число на 32-битное, используя аппаратную поддержку 64-битного и 32-битного деления. Если делитель <2 ^ 32, можно вычислить остаток, выполнив всего 4 деления, следующим образом: </p>

Предположим, что дивиденды хранятся в:

DWORD dividend[4] = ...

остаток перейдет в:

DWORD remainder;

1) Divide dividend[3] by divisor. Store the remainder in remainder.
2) Divide QWORD (remainder:dividend[2]) by divisor. Store the remainder in remainder.
3) Divide QWORD (remainder:dividend[1]) by divisor. Store the remainder in remainder.
4) Divide QWORD (remainder:dividend[0]) by divisor. Store the remainder in remainder.

После этих 4 шагов переменная остаток будет содержать то, что вы ищете. (Пожалуйста, не убивайте меня, если я ошибся с порядком байтов. Я даже не программист)

Если делитель больше 2 ^ 32-1, у меня нет хороших новостей. У меня нет полного доказательства того, что результат после сдвига отключен не более чем на 1, в процедуре, которую я описал ранее, которую я считаю MSVC использует. Однако я думаю, что это как-то связано с тем фактом, что отбрасываемая часть по меньшей мере в 2 ^ 31 раз меньше делителя, дивиденд меньше 2 ^ 64 и делитель больше 2 ^ 32-1 так что результат меньше 2 ^ 32.

Если дивиденд имеет 128 битов, трюк с отбрасыванием битов не сработает. Так что в общем случае лучшее решение, вероятно, это решение, предложенное GJ или caf. (Ну, это было бы, вероятно, лучше, даже если бы отбрасывание битов работало. Деление, умножение и вычитание умножения на 128-битное целое может быть медленнее.)

Я также думал об использовании оборудования с плавающей запятой. Модуль с плавающей запятой x87 использует формат точности 80 бит с длиной дроби 64 бита. Я думаю, что можно получить точный результат 64-битного 64-битного деления. (Не остаток непосредственно, а также остаток с использованием умножения и вычитания, как в «процедуре MSVC»). ЕСЛИ дивиденд> = 2 ^ 64 и <2 ^ 128, сохраняя его в формате с плавающей запятой, похоже на отбрасывание младших значащих битов в «процедуре MSVC». Может быть, кто-то может доказать, что ошибка в этом случае связана и найдет ее полезной. Я понятия не имею, если у него есть шанс быть быстрее, чем решение GJ, но, возможно, стоит попробовать. </p>

4 голосов
/ 07 апреля 2010

Решение зависит от того, что именно вы пытаетесь решить.

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


Чтобы дать только очень приблизительную оценку скорости этого сокращения Монтгомери: у меня есть старый тест, который выполняет модульное возведение в степень с 64-разрядным модулем и показателем в 1600 нс на 2,4 ГГц Core 2. Это возведение делает приблизительно 96 модульные умножения (и модульные сокращения) и, следовательно, требуется около 40 циклов на модульное умножение.

3 голосов
/ 01 сентября 2016

Мне известен вопрос указанного 32-битного кода, но ответ для 64-битного может быть полезен или интересен другим.

И да, 64b / 32b => 32b деление делает полезный строительный блок для 128b% 64b => 64b. __umoddi3 в libgcc (источник связан ниже) дает представление о том, как это делать, но он реализует только 2N% 2N => 2N поверх деления 2N / N => N, а не 4N% 2N => 2N .

Доступны более широкие библиотеки с высокой точностью, например, https://gmplib.org/manual/Integer-Division.html#Integer-Division.


GNU C на 64-битных машинах предоставляет функции __int128 type и libgcc для максимально эффективного умножения и деления в целевой архитектуре.

x86-64's div r/m64 инструкция делает деление 128b / 64b => 64b (также производит остаток в качестве второго вывода), но не работает, если частное переполнение. Так что вы не можете напрямую использовать его, если A/B > 2^64-1, но вы можете заставить gcc использовать его для себя (или даже встроить тот же код, который использует libgcc).


Компилирует ( проводник компилятора Godbolt ) в одну или две div инструкции (которые происходят внутри вызова функции libgcc ). Если бы существовал более быстрый способ, libgcc, вероятно, использовал бы его вместо этого.

#include <stdint.h>
uint64_t AmodB(unsigned __int128 A, uint64_t B) {
  return A % B;
}

Функция __umodti3, которую она вызывает, вычисляет полное 128b / 128b по модулю, но реализация этой функции проверяет особый случай, когда старшая половина делителя равна 0, как вы можете увидеть в источнике libgcc . (libgcc создает версию функции si / di / ti из этого кода в соответствии с целевой архитектурой. udiv_qrnnd - это встроенный макрос asm, который выполняет беззнаковое деление 2N / N => N для целевая архитектура.

Для x86-64 (и других архитектур с инструкцией аппаратного деления), fast-path (когда high_half(A) < B; гарантия div не выйдет из строя) - это всего лишь две неиспользованные ветви , некоторый пух для процессоров, вышедших из строя, и одна инструкция div r64, которая занимает около 50-100 циклов на современном x86 Процессоры, согласно таблицам insn Agner Fog . Некоторая другая работа может выполняться параллельно с div, но целочисленная единица деления не очень конвейерна и div декодирует до большого числа мопов (в отличие от деления FP).

Резервный путь все еще использует только две 64-битные div инструкции для случая, когда B только 64-битная, но A/B не умещается в 64-битной, поэтому A/B напрямую может привести к ошибке.

Обратите внимание, что __umodti3 в libgcc просто вставляет __udivmoddi4 в оболочку, которая возвращает только остаток.

* * 1068

Для повторяющихся по модулю того же B

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

uint64_t modulo_by_constant64(uint64_t A) { return A % 0x12345678ABULL; }

    movabs  rdx, -2233785418547900415
    mov     rax, rdi
    mul     rdx
    mov     rax, rdx             # wasted instruction, could have kept using RDX.
    movabs  rdx, 78187493547
    shr     rax, 36            # division result
    imul    rax, rdx           # multiply and subtract to get the modulo
    sub     rdi, rax
    mov     rax, rdi
    ret
Инструкция

x86 mul r64 выполняет умножение 64b * 64b => 128b (rdx: rax) и может использоваться как строительный блок для построения умножения 128b * 128b => 256b для реализации того же алгоритма. Поскольку нам нужна только верхняя половина полного результата 256b, это экономит несколько умножений.

Современные процессоры Intel имеют очень высокую производительность mul: задержка 3c, одна на тактовую пропускную способность. Однако точная комбинация требуемых сдвигов и добавлений зависит от константы, поэтому общий случай вычисления мультипликативного обратного значения во время выполнения не столь эффективен каждый раз, когда он используется в качестве JIT-скомпилированной или статически скомпилированной версии (даже на вершине затрат на предварительные вычисления).

ИДК, где будет точка безубыточности. Для JIT-компиляции это будет больше, чем ~ 200 повторного использования, если только вы не кешируете сгенерированный код для часто используемых значений B. Для «нормального» способа это может быть в диапазоне 200 повторных использований, но IDK, как дорого было бы найти модульное мультипликативное обратное для 128-битного / 64-битного деления.

libdivide может сделать это за вас, но только для 32- и 64-битных типов. Тем не менее, это, вероятно, хорошая отправная точка.

3 голосов
/ 31 августа 2016

Принятый ответ @caf был очень приятным и высоко оцененным, но в нем есть ошибка, невиданная годами.

Чтобы помочь протестировать это и другие решения, я выкладываю тестовый комплект и делаю его вики-сообществом.

unsigned cafMod(unsigned A, unsigned B) {
  assert(B);
  unsigned X = B;
  // while (X < A / 2) {  Original code used <
  while (X <= A / 2) {
    X <<= 1;
  }
  while (A >= B) {
    if (A >= X) A -= X;
    X >>= 1;
  }
  return A;
}

void cafMod_test(unsigned num, unsigned den) {
  if (den == 0) return;
  unsigned y0 = num % den;
  unsigned y1 = mod(num, den);
  if (y0 != y1) {
    printf("FAIL num:%x den:%x %x %x\n", num, den, y0, y1);
    fflush(stdout);
    exit(-1);
  }
}

unsigned rand_unsigned() {
  unsigned x = (unsigned) rand();
  return x * 2 ^ (unsigned) rand();
}

void cafMod_tests(void) {
  const unsigned i[] = { 0, 1, 2, 3, 0x7FFFFFFF, 0x80000000, 
      UINT_MAX - 3, UINT_MAX - 2, UINT_MAX - 1, UINT_MAX };
  for (unsigned den = 0; den < sizeof i / sizeof i[0]; den++) {
    if (i[den] == 0) continue;
    for (unsigned num = 0; num < sizeof i / sizeof i[0]; num++) {
      cafMod_test(i[num], i[den]);
    }
  }
  cafMod_test(0x8711dd11, 0x4388ee88);
  cafMod_test(0xf64835a1, 0xf64835a);

  time_t t;
  time(&t);
  srand((unsigned) t);
  printf("%u\n", (unsigned) t);fflush(stdout);
  for (long long n = 10000LL * 1000LL * 1000LL; n > 0; n--) {
    cafMod_test(rand_unsigned(), rand_unsigned());
  }

  puts("Done");
}

int main(void) {
  cafMod_tests();
  return 0;
}
3 голосов
/ 10 апреля 2010

Я сделал обе версии функции деления русского крестьянина Mod128by64: классическая и оптимизированная по скорости. Оптимизированная скорость может выполнять на моем ПК с частотой 3 ГГц более 1000 000 случайных вычислений в секунду и более чем в три раза быстрее, чем классическая функция. Если мы сравним время выполнения вычисления 128 на 64 и вычисления 64 на 64 бита по модулю, то эта функция будет только примерно на 50% медленнее.

Классический русский крестьянин:

function Mod128by64Clasic(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
//   : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//edx:ebp = Divisor
//ecx = Loop counter
//Result = esi:edi
  push    ebx                     //Store registers to stack
  push    esi
  push    edi
  push    ebp
  mov     ebp, [edx]              //Load  divisor to edx:ebp
  mov     edx, [edx + 4]
  mov     ecx, ebp                //Div by 0 test
  or      ecx, edx
  jz      @DivByZero
  push    [eax]                   //Store Divisor to the stack
  push    [eax + 4]
  push    [eax + 8]
  push    [eax + 12]
  xor     edi, edi                //Clear result
  xor     esi, esi
  mov     ecx, 128                //Load shift counter
@Do128BitsShift:
  shl     [esp + 12], 1           //Shift dividend from stack left for one bit
  rcl     [esp + 8], 1
  rcl     [esp + 4], 1
  rcl     [esp], 1
  rcl     edi, 1
  rcl     esi, 1
  setc    bh                      //Save 65th bit
  sub     edi, ebp                //Compare dividend and  divisor
  sbb     esi, edx                //Subtract the divisor
  sbb     bh, 0                   //Use 65th bit in bh
  jnc     @NoCarryAtCmp           //Test...
  add     edi, ebp                //Return privius dividend state
  adc     esi, edx
@NoCarryAtCmp:
  loop    @Do128BitsShift
//End of 128 bit division loop
  mov     eax, edi                //Load result to eax:edx
  mov     edx, esi
@RestoreRegisters:
  lea     esp, esp + 16           //Restore Divisors space on stack
  pop     ebp                     //Restore Registers
  pop     edi                     
  pop     esi
  pop     ebx
  ret
@DivByZero:
  xor     eax, eax                //Here you can raise Div by 0 exception, now function only return 0.
  xor     edx, edx
  jmp     @RestoreRegisters
end;

Скорость оптимизирована русским крестьянином:

function Mod128by64Oprimized(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
//   : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//Divisor = edx:ebp
//Dividend = ebx:edx //We need 64 bits
//Result = esi:edi
//ecx = Loop counter and Dividend index
  push    ebx                     //Store registers to stack
  push    esi
  push    edi
  push    ebp
  mov     ebp, [edx]              //Divisor = edx:ebp
  mov     edx, [edx + 4]
  mov     ecx, ebp                //Div by 0 test
  or      ecx, edx
  jz      @DivByZero
  xor     edi, edi                //Clear result
  xor     esi, esi
//Start of 64 bit division Loop
  mov     ecx, 15                 //Load byte loop shift counter and Dividend index
@SkipShift8Bits:                  //Small Dividend numbers shift optimisation
  cmp     [eax + ecx], ch         //Zero test
  jnz     @EndSkipShiftDividend
  loop    @SkipShift8Bits         //Skip Compute 8 Bits unroled loop ?
@EndSkipShiftDividend:
  test    edx, $FF000000          //Huge Divisor Numbers Shift Optimisation
  jz      @Shift8Bits             //This Divisor is > $00FFFFFF:FFFFFFFF
  mov     ecx, 8                  //Load byte shift counter
  mov     esi, [eax + 12]         //Do fast 56 bit (7 bytes) shift...
  shr     esi, cl                 //esi = $00XXXXXX
  mov     edi, [eax + 9]          //Load for one byte right shifted 32 bit value
@Shift8Bits:
  mov     bl, [eax + ecx]         //Load 8 bit part of Dividend
//Compute 8 Bits unroled loop
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove0         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow0
  ja      @DividentAbove0
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow0
@DividentAbove0:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow0:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove1         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow1
  ja      @DividentAbove1
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow1
@DividentAbove1:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow1:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove2         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow2
  ja      @DividentAbove2
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow2
@DividentAbove2:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow2:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove3         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow3
  ja      @DividentAbove3
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow3
@DividentAbove3:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow3:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove4         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow4
  ja      @DividentAbove4
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow4
@DividentAbove4:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow4:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove5         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow5
  ja      @DividentAbove5
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow5
@DividentAbove5:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow5:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove6         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow6
  ja      @DividentAbove6
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow6
@DividentAbove6:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow6:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove7         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow7
  ja      @DividentAbove7
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow7
@DividentAbove7:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow7:
//End of Compute 8 Bits (unroled loop)
  dec     cl                      //Decrement byte loop shift counter
  jns     @Shift8Bits             //Last jump at cl = 0!!!
//End of division loop
  mov     eax, edi                //Load result to eax:edx
  mov     edx, esi
@RestoreRegisters:
  pop     ebp                     //Restore Registers
  pop     edi
  pop     esi
  pop     ebx
  ret
@DivByZero:
  xor     eax, eax                //Here you can raise Div by 0 exception, now function only return 0.
  xor     edx, edx
  jmp     @RestoreRegisters
end;
2 голосов
/ 08 апреля 2010

Как правило, деление происходит медленно, умножение происходит быстрее, а сдвиг битов еще быстрее. Из того, что я видел до сих пор, в большинстве ответов использовался метод грубой силы с использованием битовых сдвигов. Существует другой способ. Будет ли это быстрее, еще неизвестно (профиль AKA это).

Вместо деления умножьте на обратное. Таким образом, чтобы обнаружить A% B, сначала вычислите обратную величину B ... 1 / B. Это можно сделать с помощью нескольких циклов, используя метод сходимости Ньютона-Рафсона. Чтобы сделать это хорошо, будет зависеть от хорошего набора начальных значений в таблице.

Для получения более подробной информации о методе схождения Ньютона-Рафсона на обратной стороне, пожалуйста, обратитесь к http://en.wikipedia.org/wiki/Division_(digital)

Как только вы получите ответ, частное Q = A * 1 / B.

Остаток R = A - Q * B.

Чтобы определить, будет ли это быстрее, чем грубая сила (поскольку множителей будет намного больше, поскольку мы будем использовать 32-битные регистры для симуляции 64-битных и 128-битных чисел, профилируйте их.

Если B в вашем коде является константой, вы можете предварительно рассчитать обратную величину и просто рассчитать, используя последние две формулы. Я уверен, что это будет быстрее, чем сдвиг битов.

Надеюсь, это поможет.

...