Est-il réellement plus rapide d'utiliser dis (i << 3) + (i << 1) pour multiplier par 10 que d'utiliser i * 10 directement?
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 )
La latence est de 1 cycle pour une addition entière et de 3 cycles pour une multiplication entière . Vous pouvez trouver les latences et le débit dans l'annexe C du "Intel® 64 and IA-32 Architectures Optimization Reference Manual", qui se trouve sur http://www.intel.com/products/processor/manuals/ .
(à partir d'un texte Intel)
À l'aide de SSE, le Core i7 peut émettre des instructions d'ajout et de multiplication simultanées, ce qui entraîne un taux de pointe de 8 opérations à virgule flottante (FLOP) par cycle d'horloge
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)
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 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 valeurs x
, y
et z
, 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 les int
valeurs. Par exemple, considérez votre question:
La multiplication et la division peuvent être réalisées en utilisant des 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 bits E2 décalées vers la droite E2. 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 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".
Y a-t-il une sorte d'entrée qui ne peut pas être multipliée ou divisée de cette façon?
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.