web-dev-qa-db-fra.com

La multiplication et la division utilisant des opérateurs de décalage en C sont-elles plus rapides?

La multiplication et la division peuvent être réalisées en utilisant des opérateurs de bits, par exemple

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)

etc.

Est-il réellement plus rapide d'utiliser (i<<3)+(i<<1) pour multiplier par 10 que d'utiliser i*10 directement? Existe-t-il une sorte d’input qui ne puisse être multiplié ou divisé de cette façon?

276
eku

Réponse courte: Peu probable.

Réponse longue: votre compilateur contient un optimiseur capable de se multiplier aussi rapidement que votre architecture de processeur cible le permet. Votre meilleur choix est d’indiquer clairement votre intention au compilateur (c’est-à-dire i * 2 plutôt que i << 1) et de le laisser décider quelle est la séquence de code machine/assemblage la plus rapide. Il est même possible que le processeur lui-même ait implémenté l'instruction de multiplication sous la forme d'une séquence de décalages et d'ajouts au microcode.

En bout de ligne - ne perdez pas beaucoup de temps à vous inquiéter à ce sujet. Si vous voulez changer, changez. Si vous voulez multiplier, multipliez. Faites ce qui est sémantiquement le plus clair - vos collègues vous remercieront plus tard. Ou, plus probablement, vous maudire plus tard si vous agissez autrement.

462
Drew Hall

Juste un point de mesure concret: il y a de nombreuses années, j'ai comparé deux versions de mon algorithme de hachage:

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

et

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

Sur chaque machine sur laquelle je me suis référencé, la première était au moins aussi rapide que la seconde. De manière quelque peu surprenante, il était parfois plus rapide (par exemple, sur un Sun Sparc). Lorsque le matériel ne prend pas en charge la multiplication rapide (et la plupart ne le font pas à l'époque), le compilateur convertit la multiplication en combinaisons appropriées de quarts de travail et d'ajout/sous-traitement. Et comme il connaissait l'objectif final, il pouvait parfois le faire avec moins d'instructions que lorsque vous écriviez explicitement les décalages et les add/subs.

Notez que c'était quelque chose comme il y a 15 ans. Espérons que les compilateurs se sont améliorés depuis lors, vous pouvez donc compter sur le compilateur qui fait la bonne chose, probablement mieux que vous ne le pouvez. (Aussi, la raison pour laquelle le code a l'air si Cish, c'est qu'il y a plus de 15 ans. J'utiliserais évidemment std::string et les itérateurs de nos jours.)

90
James Kanze

En plus de toutes les autres bonnes réponses ici, laissez-moi vous indiquer une autre raison de ne pas utiliser le décalage lorsque vous voulez dire diviser ou multiplier. Je n'ai jamais vu quelqu'un introduire un bogue en oubliant la priorité relative de la multiplication et de l'addition. J'ai vu des bugs introduits lorsque les programmeurs de maintenance ont oublié que "multiplier" via un décalage est logiquement une multiplication mais pas syntaxiquement de la même priorité que la multiplication. x * 2 + z et x << 1 + z sont très différents!

Si vous travaillez sur nombres, utilisez des opérateurs arithmétiques comme + - * / %. Si vous travaillez sur des tableaux de bits, utilisez des opérateurs tels que & ^ | >>. Ne les mélange pas; une expression qui a peu à la fois de tournoiement et d'arithmétique est un bug en attente de se produire.

63
Eric Lippert

Cela dépend du processeur et du compilateur. Certains compilateurs optimisent déjà le code de cette façon, d’autres pas. Vous devez donc vérifier chaque fois que votre code doit être optimisé de cette façon.

Sauf si vous avez désespérément besoin d'optimiser, je ne brouillerais pas mon code source uniquement pour enregistrer une instruction d'assemblage ou un cycle de processeur.

48
Jens

Est-il réellement plus rapide d'utiliser say (i << 3) + (i << 1) pour multiplier par 10 que d'utiliser i * 10 directement?

Cela peut être ou ne pas être sur votre machine - si vous vous en souciez, mesurez votre utilisation réelle.

Une étude de cas - du 486 au core i7

L'analyse comparative est très difficile à faire de manière significative, mais nous pouvons examiner quelques faits. De http://www.penguin.cz/~literakl/intel/s.html#SAL et http://www.penguin.cz/~literakl/intel/i. html # IMUL nous avons une idée des cycles d’horloge x86 nécessaires au décalage et à la multiplication arithmétiques. Supposons que nous nous en tenions à "486" (le plus récent répertorié), aux registres 32 bits et immédiats, IMUL prend 13 à 42 cycles et IDIV 44. Chaque SAL en prend 2, et en ajoutant 1, donc même avec quelques-uns de ceux-ci qui se déplacent ensemble de façon superficielle comme un gagnant.

Ces jours-ci, avec le Core i7:

(de http://software.intel.com/en-us/forums/showthread.php?t=61481 )

La latence est 1 cycle pour une addition d’entier et 3 cycles pour une multiplication d’entier . Vous trouverez les latences et le débit dans l’Annexe C du "Manuel de référence de l’optimisation des architectures Intel® 64 et IA-32", qui se trouve sur http://www.intel.com/products/processor/manuals/ .

(de certains blurb Intel)

Grâce à SSE, le Core i7 peut émettre simultanément des instructions d’ajout et de multiplication, ce qui donne un taux de pointe de 8 opérations en virgule flottante (FLOP) par cycle d’horloge.

Cela vous donne une idée du chemin parcouru. Le trivia d'optimisation - comme le décalage de bit par rapport à * - qui a été pris au sérieux même dans les années 90 est désormais obsolète. Le transfert de bits est toujours plus rapide, mais dans le cas où vous effectuez tous vos changements et que vous ajoutez les résultats, le résultat est encore plus lent. Ensuite, plus d'instructions signifie plus de défauts de cache, plus de problèmes potentiels dans le traitement en pipeline, plus l'utilisation de registres temporaires peut signifier plus de sauvegarde et de restauration du contenu des registres à partir de la pile ... il devient rapidement trop compliqué de quantifier tous les impacts, mais ils sont trop complexes. principalement négatif.

fonctionnalité dans le code source vs implémentation

Plus généralement, votre question est balisée C et C++. En tant que langages de 3ème génération, ils sont spécifiquement conçus pour masquer les détails du jeu d'instructions de processeur sous-jacent. Pour satisfaire leurs normes linguistiques, ils doivent prendre en charge les opérations de multiplication et de décalage (et bien d’autres) , même si le matériel sous-jacent ne le fait pas . Dans ce cas, ils doivent synthétiser le résultat requis à l'aide de nombreuses autres instructions. De même, ils doivent fournir un support logiciel pour les opérations en virgule flottante si le CPU en manque et qu'il n'y a pas de FPU. Les processeurs modernes supportent tous * et <<, cela peut donc sembler absurdement théorique et historique, mais il importe de noter que la liberté de choix de la mise en œuvre va dans les deux sens: même si le CPU a une instruction qui implémente le opération demandée dans le code source dans le cas général, le compilateur est libre de choisir quelque chose d'autre qu'il préfère parce que c'est mieux pour le cas spécifique auquel le compilateur est confronté.

Exemples (avec un langage d'assemblage hypothétique)

source           literal approach         optimised approach
#define N 0
int x;           .Word x                xor registerA, registerA
x *= N;          move x -> registerA
                 move x -> registerB
                 A = B * immediate(0)
                 store registerA -> x
  ...............do something more with x...............

Des instructions telles que exclusive ou (xor) n’ont aucune relation avec le code source, mais rien n’est effacé avec lui-même efface tous les bits, de sorte qu’il peut être utilisé pour mettre quelque chose à 0. Le code source qui implique des adresses de mémoire ne peut pas implique toute utilisation.

Ce genre de piratage est utilisé depuis que les ordinateurs existent. Dans les débuts des 3GL, pour que les développeurs en retirent le contenu, la sortie du compilateur devait satisfaire le développeur existant, optimisant la main et optimisant la langue. communauté que le code produit n'était pas plus lent, plus prolixe ou pire. Les compilateurs ont rapidement adopté de nombreuses optimisations - ils en sont devenus un magasin plus centralisé que n’importe quel programmeur d’Assembly, bien qu’il y ait toujours une chance de rater une optimisation spécifique qui s'avère cruciale dans un cas précis - des humains peuvent parfois Sachez-le et cherchez quelque chose de mieux pendant que les compilateurs font ce qui leur est demandé jusqu'à ce que quelqu'un leur répète l'expérience.

Ainsi, même si le décalage et l’ajout sont encore plus rapides sur certains matériels, le rédacteur du compilateur a probablement déterminé exactement quand c’est à la fois sûr et bénéfique.

Maintenabilité

Si votre matériel change, vous pouvez recompiler et examiner le processeur cible et faire un autre meilleur choix, alors que vous ne voudrez probablement jamais revoir vos "optimisations" ou la liste des environnements de compilation qui doivent utiliser la multiplication et ceux qui doivent être déplacés. Pensez à toutes les "optimisations" non décalées en bits qui ont été écrites il y a plus de 10 ans et qui ralentissent maintenant le code dans lequel elles se trouvent, car il est exécuté sur des processeurs modernes ...!

Heureusement, les bons compilateurs tels que GCC peuvent généralement remplacer une série de bitshifts et d'arithmétique par une multiplication directe lorsqu'une optimisation est activée (c'est-à-dire ...main(...) { return (argc << 4) + (argc << 2) + argc; } -> imull $21, 8(%ebp), %eax) afin qu'une recompilation puisse aider même sans corriger le code, mais ce n'est pas garanti.

Étrange code binaire qui implémente la multiplication ou la division exprime beaucoup moins ce que vous tentiez de faire, alors les autres développeurs en seront gênés, et un programmeur confus aura plus de chances d’introduire des bogues ou de supprimer quelque chose d’essentiel dans le but de restaurer une santé mentale apparente. Si vous ne faites que des choses qui ne sont pas évidentes et que vous les documentez bien (mais ne documentez pas d'autres choses intuitives de toute façon), tout le monde sera plus heureux.

Solutions générales versus solutions partielles

Si vous avez des connaissances supplémentaires, telles que votre int ne stockera réellement que les valeurs x, y et z, vous pourrez peut-être élaborer quelques instructions. cela fonctionne pour ces valeurs et vous permet d'obtenir votre résultat plus rapidement que lorsque le compilateur n'a pas cette idée et a besoin d'une implémentation qui fonctionne pour toutes les valeurs int. Par exemple, considérez votre question:

La multiplication et la division peuvent être réalisées à l'aide d'opérateurs de bits ...

Vous illustrez la multiplication, mais qu'en est-il de la division?

int x;
x >> 1;   // divide by 2?

Selon la norme C++ 5.8:

-3- La valeur de E1 >> E2 correspond aux positions de bit E2 décalées à droite de E1. Si E1 a un type non signé ou si E1 a un type signé et une valeur non négative, la valeur du résultat est la partie intégrante du quotient de E1 divisée par la quantité 2 élevée à la puissance E2. Si E1 a un type signé et une valeur négative, la valeur résultante est définie par l'implémentation.

Ainsi, votre décalage de bit a un résultat d'implémentation défini lorsque x est négatif: il peut ne pas fonctionner de la même manière sur des machines différentes. Mais, / fonctionne de manière beaucoup plus prévisible. (Cela peut ne pas être parfaitement cohérent non plus, car différentes machines peuvent avoir différentes représentations de nombres négatifs, et donc des plages différentes même lorsque le même nombre de bits constitue le même nombre de bits. représentation.)

Vous pouvez dire "ça m'est égal ... que int stocke l'âge de l'employé, il ne peut jamais être négatif". Si vous avez ce type d'informations spéciales, alors oui - votre optimisation >> safe peut être dépassée par le compilateur, à moins que vous ne le fassiez explicitement dans votre code. Mais, c'est risqué et rarement utile car vous ne disposerez pas de ce type d'informations, et les autres programmeurs travaillant sur le même code ne sauront pas que vous avez pariez sur les attentes inhabituelles concernant les données que vous allez gérer ... ce qui semble être un changement totalement sûr pourrait se retourner contre vous en raison de votre "optimisation".

Existe-t-il une sorte d’input qui ne puisse être multiplié ou divisé de cette façon?

Oui ... comme mentionné ci-dessus, les nombres négatifs ont un comportement défini par la mise en oeuvre lorsqu'ils sont "divisés" par un décalage de bits.

36
Tony Delroy

Juste essayé sur ma machine en compilant ceci:

int a = ...;
int b = a * 10;

Lors du démontage, il produit une sortie:

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift

Cette version est plus rapide que votre code optimisé à la main avec un changement et une addition purs.

Vous ne savez vraiment jamais ce que le compilateur va créer, il est donc préférable d'écrire simplement une multiplication normal et de le laisser optimiser ce qu'il veut, sauf dans des cas très précis où vous savoir le compilateur ne peut pas optimiser.

32
user703016

Le changement de vitesse est généralement beaucoup plus rapide que la multiplication à un niveau d'instruction, mais vous risquez de perdre votre temps à faire des optimisations prématurées. Le compilateur peut bien effectuer ces optimisations au moment de compiletime. Le faire vous-même affectera la lisibilité et n'aura probablement aucun effet sur les performances. Cela ne vaut probablement la peine de faire ce genre de chose que si vous vous êtes profilé et avez trouvé que c'était un goulot d'étranglement.

En réalité, le truc de la division, connu sous le nom de "division magique", peut rapporter d’énormes gains. Encore une fois, vous devriez profiler d'abord pour voir si c'est nécessaire. Mais si vous l'utilisez, il existe des programmes utiles pour vous aider à comprendre quelles instructions sont nécessaires pour la même sémantique de division. Voici un exemple: http://www.masm32.com/board/index.php?topic=12421.

Voici un exemple tiré du fil de l'OP sur MASM32:

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient

Générerait:

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16
21
Mike Kwan

Les instructions Shift et Multiplier Entier ont des performances similaires sur la plupart des processeurs modernes. Les instructions Multiplier Entier étaient relativement lentes dans les années 1980, mais en général, ce n'est plus le cas. Les instructions de multiplication d'entier peuvent avoir une latence plus élevée , de sorte qu'il peut toujours y avoir des cas où un décalage est préférable. Idem pour les cas où vous pouvez garder plus d'unités d'exécution occupées (même si cela peut aller dans les deux sens).

Cependant, la division entière est encore relativement lente. Il est donc toujours avantageux d’utiliser un décalage au lieu de la division par 2 et la plupart des compilateurs l’implémenteront comme une optimisation. Notez cependant que pour que cette optimisation soit valide, le dividende doit être non signé ou doit être réputé positif. Pour un dividende négatif, le décalage et la division ne sont pas équivalents!

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}

Sortie:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3

Donc, si vous voulez aider le compilateur, assurez-vous que la variable ou l'expression du dividende est explicitement non signée.

11
Paul R

Cela dépend complètement de l'appareil cible, de la langue, de l'objectif, etc.

Pixel crunching dans un pilote de carte vidéo? Très probablement, oui!

Application métier .NET pour votre département? Absolument aucune raison de même y regarder.

Pour un jeu de haute performance sur un appareil mobile, cela vaut la peine d’être étudié, mais seulement après que des optimisations plus simples ont été effectuées.

3
Brady Moritz

Ne le faites pas sauf si vous en avez absolument besoin et que l'intention de votre code nécessite un décalage plutôt qu'une multiplication/division.

Dans une journée typique - vous pouvez potentiellement économiser quelques cycles de machine (ou perdre du temps, car le compilateur sait mieux quoi optimiser), mais le coût n'en vaut pas la peine - vous passez du temps sur des détails mineurs plutôt que sur le travail réel, la maintenance du code devient plus difficile et plus complexe. vos collègues vous maudiront.

Vous devrez peut-être le faire pour les calculs à forte charge, où chaque cycle enregistré représente des minutes d'exécution. Cependant, vous devez optimiser un emplacement à la fois et effectuer des tests de performance à chaque fois pour voir si vous avez réellement accéléré le processus ou si vous avez dépassé la logique du compilateur.

2
Kromster

Pour autant que je sache dans certaines machines, la multiplication peut nécessiter jusqu'à 16 à 32 cycles de machine. Donc, Oui, en fonction du type de machine, les opérateurs de décalage de bits sont plus rapides que la multiplication/division.

Cependant, certaines machines ont leur processeur mathématique, qui contient des instructions spéciales pour la multiplication/division.

1
iammilind

Je suis d'accord avec la réponse marquée de Drew Hall. La réponse pourrait cependant utiliser quelques notes supplémentaires.

Pour la grande majorité des développeurs de logiciels, le processeur et le compilateur ne sont plus pertinents pour la question. La plupart d'entre nous sommes bien au-delà des normes 8088 et MS-DOS. C'est peut-être seulement pertinent pour ceux qui développent encore des processeurs embarqués ...

Dans ma société de logiciels, Math (add/sub/mul/div) doit être utilisé pour toutes les mathématiques. While Shift devrait être utilisé lors de la conversion entre types de données, par exemple. ushort à octet sous la forme n >> 8 et not n/256.

1
deegee

Je pense que dans le cas où vous voulez multiplier ou diviser par une puissance de deux, vous ne pouvez pas vous tromper en utilisant des opérateurs de décalage de bits, même si le compilateur les convertit en MUL/DIV, car certains processeurs utilisent un microcode de toute façon, vous obtiendrez une amélioration, en particulier si le décalage est supérieur à 1. Ou plus explicitement, si le processeur n'a pas d'opérateur de décalage de bits, ce sera quand même un MUL/DIV, mais si le processeur a Bitshift opérateurs, vous évitez une branche de microcode et voici quelques instructions en moins.

J'écris actuellement du code qui nécessite beaucoup d'opérations de doublage/réduction de moitié parce qu'il fonctionne sur un arbre binaire dense, et il y a une opération de plus que je soupçonne pourrait être plus optimale qu'une addition - une gauche (puissance de deux ) déplacer avec un ajout. Cela peut être remplacé par un décalage à gauche et un xor si le décalage est plus large que le nombre de bits que vous souhaitez ajouter, un exemple simple est (i << 1) ^ 1, ce qui ajoute un à une valeur doublée. Ceci ne s'applique évidemment pas à un décalage à droite (puissance de la division de deux), car seul un décalage à gauche (little endian) comble le vide avec des zéros.

Dans mon code, elles sont multipliées/divisées par deux et les puissances de deux opérations sont utilisées de manière très intensive et, comme les formules sont déjà assez courtes, chaque instruction pouvant être éliminée peut constituer un gain substantiel. Si le processeur ne prend pas en charge ces opérateurs de transfert de bits, il n'y aura pas de gain, mais il n'y aura pas non plus de perte.

De plus, dans les algorithmes que j'écris, ils représentent visuellement les mouvements qui se produisent, ils sont donc plus clairs dans ce sens. Le côté gauche d'un arbre binaire est plus grand et le droit est plus petit. De plus, dans mon code, les nombres pairs et impairs ont une signification particulière, et tous les enfants de gauche dans l'arbre sont impairs et tous les enfants de main droite et la racine sont pairs. Dans certains cas, que je n'ai pas encore rencontrés, mais que, peut-être, en fait, je n'y ai même pas pensé, x & 1 peut être une opération plus optimale comparée à x% 2. x & 1 sur un nombre pair produira zéro, mais produira 1 pour un nombre impair.

Pour aller un peu plus loin que juste une identification pair/impair, si j’obtiens zéro pour x & 3, je sais que 4 est un facteur de notre nombre, et identique pour x% 7 pour 8, et ainsi de suite. Je sais que ces cas ont probablement une utilité limitée, mais il est bon de savoir que vous pouvez éviter une opération de module et utiliser une opération de logique au niveau du bit, car les opérations au niveau du bit sont presque toujours les plus rapides et les moins susceptibles d’être ambiguës pour le compilateur.

Je suis en train d’inventer le domaine des arbres binaires denses, alors j’espère que les gens ne comprendront peut-être pas la valeur de ce commentaire, car il est très rare que les gens veuillent ne réaliser que des factorisations sur des puissances de deux ou multiplier/diviser des puissances de deux.

0
Louki Sumirniy

Test Python effectuant la même multiplication 100 millions de fois contre les mêmes nombres aléatoires.

>>> from timeit import timeit
>>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)'
>>> N = 10*1000*1000
>>> timeit('x=random.randint(65536);', setup=setup_str, number=N)
1.894096851348877 # Time from generating the random #s and no opperati

>>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N)
2.2616429328918457

>>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N)
2.9485139846801758

>>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N)
2.490908145904541
>>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N)
2.4757170677185059
>>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N)
2.2316000461578369

Ainsi, si vous effectuez un décalage plutôt qu'une multiplication/division par une puissance de deux en python, vous obtenez une légère amélioration (~ 10% pour la division; ~ 1% pour la multiplication). Si c'est un non-pouvoir de deux, il y a probablement un ralentissement considérable.

Encore une fois, ces #s changeront en fonction de votre processeur, de votre compilateur (ou de votre interpréteur - dans python par souci de simplicité).

Comme pour tout le monde, n'optimisez pas prématurément. Écrivez un code très lisible, profilez-le si ce n’est pas assez rapide, puis essayez d’optimiser les parties lentes. Rappelez-vous que votre compilateur est beaucoup plus optimiste que vous.

0
dr jimbob

Que ce soit réellement plus rapide dépend du matériel et du compilateur réellement utilisé.

0

Il existe des optimisations que le compilateur ne peut pas faire car elles ne fonctionnent que pour un ensemble réduit d'entrées.

Ci-dessous, vous trouverez un exemple de code c ++ permettant d'effectuer une division plus rapide en effectuant une "multiplication par l'inverse" à 64 bits. Le numérateur et le dénominateur doivent être inférieurs à un certain seuil. Notez qu'il doit être compilé pour utiliser des instructions 64 bits pour être réellement plus rapide que la division normale.

#include <stdio.h>
#include <chrono>

static const unsigned s_bc = 32;
static const unsigned long long s_p = 1ULL << s_bc;
static const unsigned long long s_hp = s_p / 2;

static unsigned long long s_f;
static unsigned long long s_fr;

static void fastDivInitialize(const unsigned d)
{
    s_f = s_p / d;
    s_fr = s_f * (s_p - (s_f * d));
}

static unsigned fastDiv(const unsigned n)
{
    return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc;
}

static bool fastDivCheck(const unsigned n, const unsigned d)
{
    // 32 to 64 cycles latency on modern cpus
    const unsigned expected = n / d;

    // At least 10 cycles latency on modern cpus
    const unsigned result = fastDiv(n);

    if (result != expected)
    {
        printf("Failed for: %u/%u != %u\n", n, d, expected);
        return false;
    }

    return true;
}

int main()
{
    unsigned result = 0;

    // Make sure to verify it works for your expected set of inputs
    const unsigned MAX_N = 65535;
    const unsigned MAX_D = 40000;

    const double ONE_SECOND_COUNT = 1000000000.0;

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;
    printf("Verifying...\n");
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            count += !fastDivCheck(n, d);
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += fastDiv(n);
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    count = 0;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += n / d;
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    getchar();
    return result;
}
0
user2044859

Si vous comparez les résultats des syntaxes x + x, x * 2 et x << 1 sur un compilateur gcc, vous obtiendrez le même résultat sous x86 Assembly: https://godbolt.org/z/JLpp0j

        Push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        pop     rbp
        ret

Ainsi, vous pouvez considérer gcc comme intelligent assez pour déterminer sa meilleure solution indépendamment de ce que vous avez tapé.

0
Buridan

Dans le cas des entiers signés et du décalage droit contre division, cela peut faire la différence. Pour les nombres négatifs, le décalage arrondit à l'infini négatif tandis que la division arrondit à zéro. Bien sûr, le compilateur changera la division en quelque chose de moins cher, mais le changera généralement en quelque chose qui a le même comportement d’arrondi que la division, soit parce qu’il est incapable de prouver que la variable ne sera pas négative ou tout simplement pas. se soucier. Donc, si vous pouvez prouver qu'un nombre ne sera pas négatif ou si vous ne vous souciez pas de la façon dont il va arrondir, vous pouvez effectuer cette optimisation de manière à faire une différence.

0
harold