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 disons (i<<3)+(i<<1)
pour multiplier par 10 que d'utiliser i*10
directement? Y a-t-il une sorte d'entrée qui ne peut pas être multipliée ou divisée de cette façon?
Réponses:
Réponse courte: peu probable.
Réponse longue: votre compilateur contient un optimiseur qui sait se multiplier aussi rapidement que votre architecture de processeur cible est capable. Votre meilleur pari est de dire clairement au compilateur votre intention (c'est-à-dire i * 2 plutôt que i << 1) et de le laisser décider quelle est la séquence de code assembleur / machine la plus rapide. Il est même possible que le processeur lui-même ait implémenté l'instruction multiply comme une séquence de décalages et d'ajouts dans le microcode.
Conclusion - ne passez pas beaucoup de temps à vous en préoccuper. 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 faites autrement.
la source
gcc -O3
sur x86 qu'avecreturn i*10
la version shift . En tant que personne qui regarde beaucoup la sortie du compilateur (voir beaucoup de mes réponses asm / optimisation), je ne suis pas surpris. Il y a des moments où cela peut aider à tenir le compilateur dans une façon de faire , mais ce n'est pas l'un d'entre eux. gcc est bon en mathématiques entières, car il est important.millis() >> 2
; Aurait-ce été trop demander de diviser?i / 32
vsi >> 5
eti / 4
vsi >> 2
sur gcc pour le cortex-a9 (qui n'a pas de division matérielle) avec une optimisation -O3 et l'assemblage résultant était exactement le même. Je n'aimais pas d'abord utiliser les divisions, mais cela décrit mon intention et le résultat est le même.Juste un point de mesure concret: il y a de nombreuses années, j'ai comparé deux versions de mon algorithme de hachage:
et
Sur chaque machine sur laquelle je l'ai comparé, la première était au moins aussi rapide que la seconde. Quelque peu surprenant, 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 dans les combinaisons appropriées de décalages et ajoute / sub. Et parce qu'il connaissait l'objectif final, il pouvait parfois le faire en moins d'instructions que lorsque vous écriviez explicitement les changements et les ajouts / sous-marins.
Notez que c'était quelque chose comme il y a 15 ans. J'espère que les compilateurs ne font que s'améliorer depuis lors, vous pouvez donc à peu près compter sur le compilateur qui fait la bonne chose, probablement mieux que vous. (En outre, la raison pour laquelle le code semble si Cish est parce qu'il était il y a plus de 15 ans. J'utiliserais évidemment
std::string
et les itérateurs aujourd'hui.)la source
En plus de toutes les autres bonnes réponses ici, permettez-moi de souligner une autre raison de ne pas utiliser le décalage lorsque vous entendez diviser ou multiplier. Je n'ai jamais vu quelqu'un introduire un bug en oubliant la priorité relative de la multiplication et de l'addition. J'ai vu des bogues introduits lorsque les programmeurs de maintenance ont oublié que la "multiplication" via un décalage est logiquement une multiplication mais pas syntaxiquement de la même priorité que la multiplication.
x * 2 + z
etx << 1 + z
sont très différents!Si vous travaillez sur des nombres, utilisez des opérateurs arithmétiques comme
+ - * / %
. Si vous travaillez sur des tableaux de bits, utilisez des opérateurs de torsion de bits comme& ^ | >>
. Ne les mélangez pas; une expression qui a à la fois un peu de twiddling et d'arithmétique est un bug qui attend de se produire.la source
Cela dépend du processeur et du compilateur. Certains compilateurs optimisent déjà le code de cette façon, d'autres non. Vous devez donc vérifier chaque fois que votre code doit être optimisé de cette façon.
À moins que vous ayez désespérément besoin d'optimiser, je ne brouillerais pas mon code source juste pour enregistrer une instruction d'assemblage ou un cycle de processeur.
la source
>>
opérateur est plus rapide que/
et, si les valeurs signées peuvent être négatives, il est souvent aussi sémantiquement supérieur. Si l'on a besoin de la valeur quix>>4
produirait, c'est beaucoup plus clair quex < 0 ? -((-1-x)/16)-1 : x/16;
, et je ne peux pas imaginer comment un compilateur pourrait optimiser cette dernière expression en quelque chose de bien.Il peut ou non être sur votre machine - si vous vous en souciez, mesurez dans votre utilisation réelle.
Une étude de cas - de 486 à Core i7
L'analyse comparative est très difficile à faire de manière significative, mais nous pouvons examiner quelques faits. Sur 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écessaire pour le décalage arithmétique et la multiplication. Supposons que nous nous en tenions à "486" (le plus récent répertorié), aux registres 32 bits et aux intermédiaires, IMUL prend 13 à 42 cycles et IDIV 44. Chaque SAL en prend 2 et en ajoute 1, donc même avec quelques-uns d'entre eux, le décalage superficiel semble comme un gagnant.
De nos jours, avec le Core i7:
(depuis http://software.intel.com/en-us/forums/showthread.php?t=61481 )
(à partir d'un texte Intel)
Cela vous donne une idée du chemin parcouru. Anecdote sur l'optimisation - comme le décalage de bits par rapport
*
- qui a été pris au sérieux même dans les années 90 est maintenant obsolète. Le décalage de bits est encore plus rapide, mais pour les mul / div sans puissance de deux au moment où vous effectuez tous vos changements et ajoutez les résultats, il est à nouveau plus lent. Ensuite, plus d'instructions signifie plus de défauts de cache, plus de problèmes potentiels dans le pipelining, plus d'utilisation de registres temporaires peut signifier plus de sauvegarde et de restauration du contenu du registre de la pile ... cela devient rapidement trop compliqué pour quantifier définitivement tous les impacts mais ils sont 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 3e génération, ils sont spécifiquement conçus pour masquer les détails du jeu d'instructions CPU 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 de tels cas, ils doivent synthétiser le résultat requis en utilisant de nombreuses autres instructions. De même, ils doivent fournir un support logiciel pour les opérations en virgule flottante si le processeur en manque et qu'il n'y a pas de FPU. Les processeurs modernes prennent tous en charge
*
et<<
, donc cela peut sembler absurdement théorique et historique, mais la chose importante est que la liberté de choisir l'implémentation va dans les deux sens: même si le CPU a une instruction qui implémente l'opération demandée dans le code source dans le cas général, le compilateur est libre de choisissez autre chose qu'il préfère, car c'est mieux pour le cas spécifique auquel le compilateur est confronté.Exemples (avec un langage d'assemblage hypothétique)
Des instructions telles que exclusive ou (
xor
) n'ont aucune relation avec le code source, mais tout ce qui est effacé lui-même efface tous les bits, il peut donc être utilisé pour mettre quelque chose à 0. Le code source qui implique des adresses mémoire ne peut impliquer aucune utilisation.Ce type de piratage est utilisé depuis aussi longtemps que les ordinateurs existent. Dans les premiers jours des 3GL, pour sécuriser l'adoption par les développeurs, la sortie du compilateur devait satisfaire le développeur existant en langage d'assemblage optimisant la main. communauté que le code produit n'était pas plus lent, plus verbeux ou pire. Les compilateurs ont rapidement adopté beaucoup de grandes optimisations - ils en sont devenus un meilleur stockage centralisé que tout programmeur individuel en langage d'assemblage pourrait être, bien qu'il y ait toujours la possibilité qu'ils manquent une optimisation spécifique qui s'avère cruciale dans un cas spécifique - les humains peuvent parfois écraser et tâtonner pour quelque chose de mieux tandis que les compilateurs font juste ce qu'on leur a dit jusqu'à ce que quelqu'un leur fasse revivre cette expérience.
Donc, même si le décalage et l'ajout sont encore plus rapides sur un matériel particulier, le rédacteur du compilateur a probablement fonctionné exactement quand il est à la fois sûr et bénéfique.
Maintenabilité
Si votre matériel change, vous pouvez recompiler et il examinera le processeur cible et fera un autre meilleur choix, alors que vous ne voudrez probablement jamais revoir vos "optimisations" ou répertorier les environnements de compilation qui devraient utiliser la multiplication et ceux qui devraient changer. Pensez à toutes les «optimisations» décalées non-puissance de deux écrites il y a plus de 10 ans qui ralentissent maintenant le code dans lequel il se trouve lorsqu'il fonctionne sur des processeurs modernes ...!
Heureusement, de bons compilateurs comme GCC peuvent généralement remplacer une série de décalages de bits et d'arithmétique par une multiplication directe lorsque toute optimisation est activée (c'est
...main(...) { return (argc << 4) + (argc << 2) + argc; }
-à- dire ->imull $21, 8(%ebp), %eax
), donc une recompilation peut aider même sans corriger le code, mais ce n'est pas garanti.Un code de décalage de bits étrange implémentant la multiplication ou la division est beaucoup moins expressif de ce que vous tentiez de réaliser conceptuellement, de sorte que d'autres développeurs seront confus par cela, et un programmeur confus est plus susceptible d'introduire des bogues ou de supprimer quelque chose d'essentiel dans un effort pour restaurer une apparence saine. Si vous ne faites des choses non évidentes que lorsqu'elles sont vraiment tangibles, puis 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 quelques connaissances supplémentaires, par exemple que votre
int
volonté soit vraiment seulement stocker des valeursx
,y
etz
, alors vous pouvez être en mesure d'élaborer des instructions de travail pour ces valeurs et vous obtenez votre résultat plus rapidement que lorsque n'a pas de compilateur cet aperçu et a besoin d'une mise en œuvre qui fonctionne pour toutes lesint
valeurs. Par exemple, considérez votre question:Vous illustrez la multiplication, mais qu'en est-il de la division?
Selon la norme C ++ 5.8:
Ainsi, votre décalage de bits a un résultat défini par l'implémentation lorsqu'il
x
est négatif: il peut ne pas fonctionner de la même manière sur différentes machines. Mais,/
fonctionne de manière beaucoup plus prévisible. (Il 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 lorsqu'il y a le même nombre de bits constituant la représentation.)Vous pouvez dire "Je m'en fiche ... c'est
int
mémoriser l'âge de l'employé, ça ne peut jamais être négatif". Si vous avez ce genre d'informations particulières, alors oui - votre>>
optimisation sûre peut être ignorée par le compilateur, sauf si vous le faites explicitement dans votre code. Mais, c'est risqué et rarement utile la plupart du temps, vous n'aurez pas ce genre de perspicacité, et les autres programmeurs travaillant sur le même code ne sauront pas que vous avez parié la maison sur des attentes inhabituelles des données que vous '' Je vais gérer ... ce qui semble être un changement totalement sûr pourrait se retourner contre vous à cause de votre "optimisation".Oui ... comme mentionné ci-dessus, les nombres négatifs ont un comportement défini par l'implémentation lorsqu'ils sont "divisés" par décalage de bits.
la source
intVal>>1
aura la même sémantique qui diffère de celles d'intVal/2
une manière parfois utile. Si l'on doit calculer de manière portable la valeur que donneraient les architectures courantesintVal >> 1
, l'expression devrait être plutôt plus compliquée et plus difficile à lire, et serait susceptible de générer un code sensiblement inférieur à celui produit pourintVal >> 1
.Je viens d'essayer sur ma machine de compiler ceci:
Lors du démontage, il produit une sortie:
Cette version est plus rapide que votre code optimisé à la main avec un décalage et un ajout purs.
Vous ne savez jamais vraiment ce que le compilateur va proposer, il est donc préférable d'écrire simplement une multiplication normale et de le laisser optimiser comme il le souhaite, sauf dans des cas très précis où vous savez que le compilateur ne peut pas optimiser.
la source
vector<T>::size()
. Mon compilateur était assez ancien! :)Le décalage est généralement beaucoup plus rapide que la multiplication au niveau de l'instruction, mais vous perdez peut-être votre temps à faire des optimisations prématurées. Le compilateur peut très bien effectuer ces optimisations au moment de la compilation. Le faire vous-même affectera la lisibilité et n'aura probablement aucun effet sur les performances. Cela vaut probablement la peine de faire des choses comme ça si vous avez profilé et trouvé que c'était un goulot d'étranglement.
En fait, l'astuce de division, connue sous le nom de «division magique», peut en fait générer d'énormes gains. Encore une fois, vous devez d'abord profiler pour voir si cela 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.0
Un exemple que j'ai retiré du fil de l'OP sur MASM32:
Générerait:
la source
Les instructions de décalage et de multiplication d'entiers ont des performances similaires sur la plupart des processeurs modernes - les instructions de multiplication d'entiers étaient relativement lentes dans les années 1980, mais en général, ce n'est plus le cas. Les instructions de multiplication d'entiers peuvent avoir une latence plus élevée , il peut donc toujours y avoir des cas où un décalage est préférable. Idem pour les cas où vous pouvez occuper plus d'unités d'exécution (même si cela peut aller dans les deux sens).
La division entière est encore relativement lente, donc utiliser un décalage au lieu de la division par une puissance de 2 est toujours une victoire, et la plupart des compilateurs implémenteront cela comme une optimisation. Notez cependant que pour que cette optimisation soit valide, le dividende doit être non signé ou doit être connu pour être positif. Pour un dividende négatif, le décalage et la division ne sont pas équivalents!
Production:
Donc, si vous voulez aider le compilateur, assurez-vous que la variable ou l'expression dans le dividende n'est pas explicitement signée.
la source
Cela dépend complètement de l'appareil cible, de la langue, du but, etc.
Pixel craquant 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 jeter un œil.
Pour un jeu de haute performance pour un appareil mobile, il peut être utile de l'examiner, mais seulement après avoir effectué des optimisations plus faciles.
la source
Ne le faites pas sauf si vous en avez absolument besoin et que votre intention de code nécessite un décalage plutôt qu'une multiplication / division.
Dans la journée typique - vous pourriez potentiellement économiser quelques cycles de machine (ou perdre, 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, maintenir le code devient plus difficile et vos collègues vous maudiront.
Vous devrez peut-être le faire pour les calculs à haute charge, où chaque cycle enregistré signifie des minutes d'exécution. Mais, vous devez optimiser un endroit à la fois et faire des tests de performances à chaque fois pour voir si vous avez vraiment accéléré ou cassé la logique des compilateurs.
la source
Pour autant que je sache, sur certaines machines, la multiplication peut nécessiter jusqu'à 16 à 32 cycles machine. Alors oui , selon le 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.
la source
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 sont bien au-delà du 8088 et du MS-DOS. Il n'est peut-être pertinent que pour ceux qui développent encore des processeurs embarqués ...
Dans ma société de logiciels, Math (add / sub / mul / div) devrait être utilisé pour toutes les mathématiques. Alors que Shift doit être utilisé lors de la conversion entre les types de données, par exemple. ushort à octet comme n >> 8 et non n / 256.
la source
Dans le cas d'entiers signés et de décalage à droite vs division, cela peut faire une différence. Pour les nombres négatifs, le décalage arrondit vers l'infini négatif tandis que la division arrondit vers zéro. Bien sûr, le compilateur changera la division en quelque chose de moins cher, mais il le changera généralement en quelque chose qui a le même comportement d'arrondi que la division, car il est incapable de prouver que la variable ne sera pas négative ou simplement non 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 sera arrondi, vous pouvez faire cette optimisation d'une manière plus susceptible de faire la différence.
la source
unsigned
Test Python effectuant la même multiplication 100 millions de fois contre les mêmes nombres aléatoires.
Donc, en faisant un décalage plutôt que la multiplication / division par une puissance de deux en python, il y a une légère amélioration (~ 10% pour la division; ~ 1% pour la multiplication). Si c'est une non-puissance 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 interprète - fait en python pour plus de simplicité).
Comme pour tout le monde, n'optimisez pas prématurément. Écrivez du code très lisible, profilez s'il n'est pas assez rapide, puis essayez d'optimiser les parties lentes. N'oubliez pas que votre compilateur est bien meilleur en optimisation que vous.
la source
Il y a des optimisations que le compilateur ne peut pas faire car elles ne fonctionnent que pour un ensemble réduit d'entrées.
Ci-dessous, il y a un exemple de code c ++ qui peut effectuer une division plus rapide en effectuant une "multiplication par la réciproque" de 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.
la source
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, parce que certains microcodes de processeurs (vraiment, un macro) de toute façon, donc dans ces cas, vous obtiendrez une amélioration, surtout si le décalage est supérieur à 1. Ou plus explicitement, si le CPU n'a pas d'opérateurs de décalage de bit, ce sera de toute façon un MUL / DIV, mais si le CPU a opérateurs bithift, vous évitez une branche de microcode et c'est quelques instructions de moins.
J'écris actuellement du code qui nécessite beaucoup d'opérations de doublement / réduction de moitié car il fonctionne sur un arbre binaire dense, et il y a une opération de plus que je soupçonne peut-être plus optimale qu'une addition - une gauche (puissance de deux multiplier ) décalage avec un ajout. Cela peut être remplacé par un décalage vers la 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, qui ajoute un à une valeur doublée. Cela ne s'applique bien sûr pas à un décalage à droite (puissance de deux) car seul un décalage à gauche (petit endian) remplit l'espace avec des zéros.
Dans mon code, ces multiplications / divisions par deux et les puissances de deux opérations sont utilisées de manière très intensive et parce que les formules sont déjà assez courtes, chaque instruction qui peut être éliminée peut être un gain substantiel. Si le processeur ne prend pas en charge ces opérateurs de décalage de bits, aucun gain ne se produira 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 donc en ce sens ils sont en fait plus clairs. Le côté gauche d'un arbre binaire est plus grand et le côté droit est plus petit. En plus de cela, 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 droite, et la racine, sont pairs. Dans certains cas, que je n'ai pas encore rencontrés, mais peut-être, oh, en fait, je n'y ai même pas pensé, x & 1 peut être une opération plus optimale que x% 2. x & 1 sur un nombre pair produira zéro, mais produira 1 pour un nombre impair.
Allant un peu plus loin qu'une simple identification impaire / paire, si j'obtiens zéro pour x et 3, je sais que 4 est un facteur de notre nombre, et même 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 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.
J'invente à peu près le domaine des arbres binaires denses, donc je m'attends à ce que les gens ne saisissent pas la valeur de ce commentaire, car très rarement, les gens ne veulent effectuer des factorisations que sur des puissances de deux seulement, ou seulement multiplier / diviser des puissances de deux.
la source
Qu'il soit réellement plus rapide dépend du matériel et du compilateur réellement utilisés.
la source
Si vous comparez la sortie pour la syntaxe x + x, x * 2 et x << 1 sur un compilateur gcc, vous obtiendrez le même résultat dans l'assemblage x86: https://godbolt.org/z/JLpp0j
Vous pouvez donc considérer gcc comme un outil suffisamment intelligent pour déterminer sa propre meilleure solution indépendamment de ce que vous avez tapé.
la source
Moi aussi, je voulais voir si je pouvais battre la Chambre. ceci est un bitwise plus général pour n'importe quel nombre par n'importe quelle multiplication de nombre. les macros que j'ai créées sont environ 25% plus à deux fois plus lentes que la multiplication normale *. comme disent les autres si c'est proche d'un multiple de 2 ou composé de quelques multiples de 2, vous pourriez gagner. comme X * 23 composé de (X << 4) + (X << 2) + (X << 1) + X va être plus lent que X * 65 composé de (X << 6) + X.
la source