Oto link do dokumentu algorytmu, który generuje wartości i kod, które widzę w Visual Studio (w większości przypadków) i który, jak zakładam, jest nadal używany w GCC do dzielenia zmiennej całkowitej przez stałą liczbę całkowitą.
http://gmplib.org/~tege/divcnst-pldi94.pdf
W artykule uword ma N bitów, słowo ma 2N bitów, n = licznik = dywidenda, d = mianownik = dzielnik, ℓ jest początkowo ustawiony na pułap (log2 (d)), shpre jest przed przesunięciem (używane przed pomnożeniem ) = e = liczba ostatnich zerowych bitów w d, shpost jest przesunięciem (używane po pomnożeniu), prec jest precyzją = N - e = N - shpre. Celem jest optymalizacja obliczeń n / d przy użyciu zmiany wstępnej, pomnożenia i zmiany następczej.
Przewiń w dół do rysunku 6.2, który definiuje sposób generowania mnożnika udword (maksymalny rozmiar to N + 1 bit), ale nie wyjaśnia dokładnie procesu. Wyjaśnię to poniżej.
Rysunek 4.2 i rysunek 6.2 pokazują, w jaki sposób mnożnik można zredukować do mnożnika N bitowego lub mniejszego dla większości dzielników. Równanie 4.5 wyjaśnia, w jaki sposób wyprowadzono wzór używany do radzenia sobie z mnożnikami bitów N + 1 na rysunkach 4.1 i 4.2.
W przypadku współczesnych X86 i innych procesorów czas mnożenia jest stały, więc wstępne przesunięcie nie pomaga w tych procesorach, ale nadal pomaga zmniejszyć mnożnik z N + 1 bitów do N bitów. Nie wiem, czy GCC czy Visual Studio wyeliminowały wstępne zmiany dla celów X86.
Wracając do rysunku 6.2. Licznik (dywidenda) dla mlow i mhigh może być większy niż słowo ud, tylko gdy mianownik (dzielnik)> 2 ^ (N-1) (gdy ℓ == N => mlow = 2 ^ (2N)), w tym przypadku zoptymalizowane zastąpienie dla n / d jest porównaniem (jeśli n> = d, q = 1, w innym przypadku q = 0), więc nie jest generowany mnożnik. Początkowe wartości mlow i mhigh będą wynosić N + 1 bitów, a do podziału każdej wartości bitowej N + 1 (mlow lub mhigh) można użyć dwóch podziałów udword / uword. Używanie X86 w trybie 64-bitowym jako przykład:
; upper 8 bytes of dividend = 2^(ℓ) = (upper part of 2^(N+ℓ))
; lower 8 bytes of dividend for mlow = 0
; lower 8 bytes of dividend for mhigh = 2^(N+ℓ-prec) = 2^(ℓ+shpre) = 2^(ℓ+e)
dividend dq 2 dup(?) ;16 byte dividend
divisor dq 1 dup(?) ; 8 byte divisor
; ...
mov rcx,divisor
mov rdx,0
mov rax,dividend+8 ;upper 8 bytes of dividend
div rcx ;after div, rax == 1
mov rax,dividend ;lower 8 bytes of dividend
div rcx
mov rdx,1 ;rdx:rax = N+1 bit value = 65 bit value
Możesz to przetestować za pomocą GCC. Już wiesz, jak obsługiwane jest j = i / 5. Zobacz, jak obsługiwane jest j = i / 7 (co powinno być przypadkiem mnożnika N + 1 bit).
W większości obecnych procesorów mnożenie ma ustalony czas, więc zmiana wstępna nie jest potrzebna. W przypadku X86 wynikiem końcowym jest sekwencja dwóch instrukcji dla większości dzielników i pięć sekwencji instrukcji dla dzielników takich jak 7 (w celu emulacji mnożnika bitów N + 1, jak pokazano w równaniu 4.5 i rysunku 4.2 pliku pdf). Przykładowy kod X86-64:
; rax = dividend, rbx = 64 bit (or less) multiplier, rcx = post shift count
; two instruction sequence for most divisors:
mul rbx ;rdx = upper 64 bits of product
shr rdx,cl ;rdx = quotient
;
; five instruction sequence for divisors like 7
; to emulate 65 bit multiplier (rbx = lower 64 bits of multiplier)
mul rbx ;rdx = upper 64 bits of product
sub rbx,rdx ;rbx -= rdx
shr rbx,1 ;rbx >>= 1
add rdx,rbx ;rdx = upper 64 bits of corrected product
shr rdx,cl ;rdx = quotient
; ...