Voici un lien vers un document d'un algorithme qui produit les valeurs et le code que je vois avec Visual Studio (dans la plupart des cas) et qui, je suppose, est toujours utilisé dans GCC pour la division d'un entier variable par un entier constant.
http://gmplib.org/~tege/divcnst-pldi94.pdf
Dans l'article, un uword a N bits, un udword a 2N bits, n = numérateur = dividende, d = dénominateur = diviseur, est initialement fixé à ceil(log2(d)), shpre est pre-shift (utilisé avant la multiplication) = e = nombre de bits zéro de queue dans d, shpost est post-shift (utilisé après la multiplication), prec est la précision = N - e = N - shpre. L'objectif est d'optimiser le calcul de n/d en utilisant un pré-décalage, une multiplication et un post-décalage.
Faites défiler vers le bas jusqu'à la figure 6.2, qui définit comment un multiplicateur udword (la taille maximale est de N+1 bits), est généré, mais n'explique pas clairement le processus. Je vais l'expliquer ci-dessous.
La figure 4.2 et la figure 6.2 montrent comment le multiplicateur peut être réduit à un multiplicateur de N bits ou moins pour la plupart des diviseurs. L'équation 4.5 explique comment la formule utilisée pour traiter les multiplicateurs à N+1 bits dans les figures 4.1 et 4.2 a été dérivée.
Dans le cas des processeurs modernes X86 et autres, le temps de multiplication est fixe, donc le pre-shift n'est pas utile sur ces processeurs, mais il aide toujours à réduire le multiplicateur de N+1 bits à N bits. Je ne sais pas si GCC ou Visual Studio ont éliminé le pre-shift pour les cibles X86.
Revenons à la figure 6.2. Le numérateur (dividende) pour mlow et mhigh peut être plus grand qu'un udword seulement quand le dénominateur (diviseur) > 2^(N-1) (quand == N => mlow = 2^(2N)), dans ce cas le remplacement optimisé pour n/d est une comparaison (si n>=d, q = 1, sinon q = 0), donc aucun multiplicateur n'est généré. Les valeurs initiales de mlow et mhigh seront de N+1 bits, et deux divisions udword/uword peuvent être utilisées pour produire chaque valeur de N+1 bits (mlow ou mhigh). En utilisant X86 en mode 64 bits comme exemple :
; 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
Vous pouvez tester cela avec GCC. Vous avez déjà vu comment j = i/5 est traité. Regardez comment j = i/7 est traité (ce qui devrait être le cas du multiplicateur à N+1 bits).
Sur la plupart des processeurs actuels, la multiplication a un timing fixe, donc un pre-shift n'est pas nécessaire. Pour X86, le résultat final est une séquence de deux instructions pour la plupart des diviseurs, et une séquence de cinq instructions pour les diviseurs comme 7 (afin d'émuler un multiplicateur à N+1 bits comme indiqué dans l'équation 4.5 et la figure 4.2 du fichier pdf). Exemple de code 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
; ...
30 votes
Gcc optimise les divisions par des constantes, essayez les divisions par 2,3,4,5,6,7,8 et vous verrez très probablement un code très différent pour chaque cas.
2 votes
Essayez de lire les valeurs de l'utilisateur pour voir des instructions de division réelles.
0 votes
Hm, c'est étrange, j'ai désactivé les optimisations avec
-O0
et il est toujours optimisé ?31 votes
Note : Nombre magique
-3689348814741910323
se convertit enCCCCCCCCCCCCCCCD
en tant queuint64_t
ou à peu près (2^64)*4/5.33 votes
@qiubit : Le compilateur ne générera pas perversement du code inefficace juste parce que l'optimisation est désactivée. Une "optimisation" triviale qui n'implique pas de réordonnancement du code ou d'élimination de variables sera effectuée malgré tout, par exemple. Essentiellement, une seule déclaration source se traduira par le code le plus efficace pour cette opération prise isolément. L'optimisation du compilateur tient compte du code environnant plutôt que de la seule instruction.
22 votes
Lisez cet article génial : Le travail de division
12 votes
Certains compilateurs sera génèrent perversement du code inefficace parce que l'optimisation est désactivée. En particulier, ils le feront pour faciliter le débogage, comme la possibilité de définir des points d'arrêt sur des lignes de code individuelles. GCC est, en fait, plutôt inhabituel dans la mesure où il ne dispose pas d'un véritable mode "sans optimisations", car beaucoup de ses optimisations sont activées de manière constitutive. Voici un exemple où vous pouvez voir cela avec GCC. Clang, d'autre part, et MSVC, sera émettent un
div
l'enseignement à-O0
. (cc @ clifford)3 votes
Ce qui est important ici, c'est qu'il ne s'agit que de l'une des nombreuses façons possibles d'implémenter un opérateur C unique avec les mêmes entrées que la machine C abstraite. Cela n'a aucun impact sur le débogage, car il n'y a pas d'optimisation sur plusieurs instructions ou quoi que ce soit d'autre. Il existe des architectures qui n'ont pas d'instructions de division matérielle.
gcc -O0
a cette astuce activée (pour toutes les architectures) afin que la division par des constantes puisse être compilée de manière saine sur ces cibles.3 votes
BTW, en utilisant
-Os
(optimiser pour un petit code) fera en sorte que gcc utilise DIV au lieu d'un inverse multiplicatif modulaire : godbolt.org/g/FPB74p . Clang utilise toujours un inverse multiplicatif, même si cela prend beaucoup d'instructions. Il s'agit d'une augmentation minime de la taille du code pour les petites constantes comme 13, cependant. (Voir à la fois gcc et clang pour /13 et /12345 dans ce lien godbolt, comme des fonctions qui prennent des arguments et retournent une valeur, donc ils n'optimisent pas la division comme votremain()
exemple).0 votes
Ce que je ne comprends pas vraiment ici, c'est pourquoi le compilateur génère du code pour effectuer une division (optimisée). Les valeurs sont des constantes, donc le résultat pourrait être calculé pendant la compilation, non ? Pour voir une instruction de division générique réelle, je suggère de faire en sorte que le programme lise les valeurs de i et j.
2 votes
@jamesqf Opérer sur les constantes est le genre de chose qui, si vous compilez avec
-O0
GCC suppose que vous le voulez pour une raison quelconque. Il y a très peu de méthode pour cela, cependant.2 votes
Le CCG doit fournir
-O-1
y-O-2
des options pour générer délibérément du code inefficace ;-)1 votes
@fuz je ne sais pas si cela précis question dupliquée ailleurs, mais elle est répondu implicitement dans le cadre de stackoverflow.com/questions/40354978's réponses. On peut également y répondre directement à stackoverflow.com/a/12909900/616460 (et cette réponse est en fait un peu plus intéressante que celles données ici, je pense, car elle décrit comment trouver les nombres magiques). Il est également assez facile à trouver sur Google (cette dernière réponse était le premier résultat de recherche pour "gcc integer division assembler")...
0 votes
Voir aussi stackoverflow.com/questions/3850665/
0 votes
@Sneftel : Bien sûr, j'ai compris qu'il n'y a pas d'optimisation, mais alors pourquoi faire l'optimisation en remplaçant les DIV par des shifts &c ? Très peu de méthode, en effet :-)
2 votes
@CodyGray Cependant, cette optimisation particulière n'affecte aucun débogueur à ma connaissance. Bien sûr, on s'attendrait à ce que "aucune optimisation" évite de déplacer des lignes de code ou de les entrelacer, mais ce n'est pas le cas.
1 votes
Et puis vous réalisez que les humains optimisent leur division avec des tables de consultation...
0 votes
Diviser un nombre par 5 sans utiliser l'opérateur de division , Quel est le moyen le plus rapide de diviser un nombre entier par 3 ? , C++ division rapide/modification par 10^x , Comment laisser le compilateur GCC transformer la division de variable en mul(si plus rapide)
0 votes
Pour i/7, le code est un peu plus compliqué : mov r8, i | mov rax, 2635249153387078803 | mul r8 | sub r8, rdx | shr r8, 1 | add rdx, r8 | shr rdx, 2 | mov rax,rdx .
1 votes
"Ce que je ne comprends pas vraiment ici, c'est pourquoi le compilateur génère du code pour faire une division (optimisée) à tous". AIUI gcc à O0 optimisera dans une déclaration mais pas entre les déclarations. Si nous changeons la division en 9/5, elle est optimisée. Si nous changeons la division pour que les deux entrées soient des variables, il génère une instruction div.
0 votes
Notez que si vous essayez de remplacer l'instruction de division de l'assembleur, qui, par exemple, divise un entier de 128 bits par un entier de 64 bits, le multiplicateur "magique" sera de 128 ou 129 bits, ce qui nécessite 4 multiplications, puis un décalage vers la droite des 128 bits supérieurs du produit de 256 bits. Pour un multiplicateur de 129 bits, il s'agit toujours de 4 multiplications, avec une soustraction, un décalage vers la droite de 1 bit, une addition, un décalage vers la droite d'un nombre fixe de bits. Cela reste utile sur des processeurs comme le X86 actuel où la multiplication est plus de 4 fois plus rapide que la division.