Y a-t-il un gain de performance (non-microoptimisation) en codant
float f1 = 200f / 2
par rapport à
float f2 = 200f * 0.5
Un de mes professeurs m'a dit il y a quelques années que les divisions en virgule flottante étaient plus lentes que les multiplications en virgule flottante sans expliquer pourquoi.
Cette affirmation vaut-elle pour l'architecture PC moderne?
pdate1
En ce qui concerne un commentaire, veuillez également considérer ce cas:
float f1;
float f2 = 2
float f3 = 3;
for( i =0 ; i < 1e8; i++)
{
f1 = (i * f2 + i / f3) * 0.5; //or divide by 2.0f, respectively
}
pdate 2 Citant les commentaires:
[Je veux] savoir quelles sont les exigences algorithmiques/architecturales qui> rendent la division beaucoup plus compliquée en matériel que la multiplication
Oui, de nombreux processeurs peuvent effectuer une multiplication en 1 ou 2 cycles d'horloge, mais la division prend toujours plus de temps (bien que FP est parfois plus rapide que la division entière).
Si vous regardez cette réponse vous verrez que la division peut dépasser 24 cycles.
Pourquoi la division prend-elle autant de temps que la multiplication? Si vous vous souvenez de la rentrée, vous vous souvenez peut-être que la multiplication peut essentiellement être effectuée avec de nombreux ajouts simultanés. La division nécessite une soustraction itérative qui ne peut pas être effectuée simultanément, ce qui prend plus de temps. En fait, certaines unités FP accélèrent la division en effectuant une approximation réciproque et en la multipliant. Elle n'est pas aussi précise mais est un peu plus rapide.
La division est intrinsèquement une opération beaucoup plus lente que la multiplication.
Et cela peut en fait être quelque chose que le compilateur ne peut pas (et vous ne voudrez peut-être pas) optimiser dans de nombreux cas en raison d'inexactitudes en virgule flottante. Ces deux déclarations:
double d1 = 7 / 10.;
double d2 = 7 * 0.1;
sont pas sémantiquement identiques - 0.1
ne peut pas être représenté exactement comme un double
, donc une valeur légèrement différente finira par être utilisée - substituer la multiplication pour la division dans ce cas donnerait un résultat différent!
Soyez très prudent avec la division et évitez-la si possible. Par exemple, sortez float inverse = 1.0f / divisor;
D'une boucle et multipliez par inverse
à l'intérieur de la boucle. (Si l'erreur d'arrondi dans inverse
est acceptable)
Habituellement, 1.0/x
Ne sera pas exactement représentable en tant que float
ou double
. Ce sera exact lorsque x
est une puissance de 2. Cela permet aux compilateurs d'optimiser x / 2.0f
En x * 0.5f
Sans aucun changement dans le résultat.
Pour laisser le compilateur effectuer cette optimisation pour vous même lorsque le résultat n'est pas exact (ou avec un diviseur à variable d'exécution), vous avez besoin d'options comme gcc -O3 -ffast-math
. Plus précisément, -freciprocal-math
(Activé par -funsafe-math-optimizations
Activé par -ffast-math
) Permet au compilateur de remplacer x / y
Par x * (1/y)
lorsque cela est utile. D'autres compilateurs ont des options similaires, et ICC peut permettre une optimisation "dangereuse" par défaut (je pense que oui, mais j'oublie).
-ffast-math
Est souvent important pour permettre la vectorisation automatique des boucles FP, en particulier les réductions (par exemple, la somme d'un tableau en un total scalaire), car FP = les mathématiques ne sont pas associatives. Pourquoi GCC n'optimise-t-il pas * a * a * a * a * a à (a * a * a) * (a * a * a)?
Notez également que les compilateurs C++ peuvent plier +
Et *
Dans un FMA dans certains cas (lors de la compilation pour une cible qui le prend en charge, comme -march=haswell
), Mais ils ne peuvent pas faites cela avec /
.
La division a une latence pire que la multiplication ou l'addition (ou FMA ) par un facteur de 2 à 4 sur les processeurs x86 modernes, et pire rendement d'un facteur de 6 à 401 (pour une boucle serrée faisant seulement division au lieu de seulement multiplication).
L'unité divide/sqrt n'est pas entièrement canalisée, pour les raisons expliquées dans @ réponse de NathanWhitehead . Les pires ratios concernent les vecteurs 256b, car (contrairement aux autres unités d'exécution), l'unité de division n'est généralement pas pleine largeur, donc les vecteurs larges doivent être effectués en deux moitiés. Une unité d'exécution non entièrement pipelinée est si inhabituelle que les processeurs Intel ont un compteur de performances matérielles arith.divider_active
Pour vous aider à trouver du code qui goulet d'étranglement sur le débit du diviseur au lieu des goulots d'étranglement habituels du port frontal ou d'exécution. (Ou plus souvent, des goulots d'étranglement de la mémoire ou de longues chaînes de latence limitant le parallélisme au niveau de l'instruction entraînant un débit d'instruction inférieur à ~ 4 par horloge).
Cependant, FP division et sqrt sur les processeurs Intel et AMD (autres que KNL) est implémenté comme une seule uop, donc il n'a pas nécessairement un grand impact du débit sur le code environnant . Le meilleur cas pour la division est lorsque l'exécution dans le désordre peut masquer la latence, et lorsqu'il y a beaucoup de multiplications et d'ajouts (ou d'autres travaux) qui peuvent se produire en parallèle avec la fracture.
(La division entière est microcodée comme plusieurs uops sur Intel, donc cela a toujours plus d'impact sur le code environnant que les nombres entiers se multiplient. Il y a moins de demande pour la division entière haute performance, donc le support matériel n'est pas aussi sophistiqué. Lié: microcodé des instructions comme idiv
peuvent provoquer des goulots d'étranglement frontaux sensibles à l'alignement .)
Ainsi, par exemple, ce sera vraiment mauvais:
for ()
a[i] = b[i] / scale; // division throughput bottleneck
// Instead, use this:
float inv = 1.0 / scale;
for ()
a[i] = b[i] * inv; // multiply (or store) throughput bottleneck
Tout ce que vous faites dans la boucle, c'est charger/diviser/stocker, et ils sont indépendants, donc c'est le débit qui compte, pas la latence.
Une réduction comme accumulator /= b[i]
Goulot d'étranglement sur la division ou la multiplication de la latence, plutôt que sur le débit. Mais avec plusieurs accumulateurs que vous divisez ou multipliez à la fin, vous pouvez masquer la latence tout en saturant le débit. Notez que sum += a[i] / b[i]
Goulots d'étranglement sur la latence de add
ou le débit de div
, mais pas la latence de div
car la division n'est pas sur le chemin critique (la boucle transportée par la boucle chaîne de dépendance).
Mais dans quelque chose comme ça ( approximant une fonction comme log(x)
avec un rapport de deux polynômes ), la division peut être assez bon marché :
for () {
// (not shown: extracting the exponent / mantissa)
float p = polynomial(b[i], 1.23, -4.56, ...); // FMA chain for a polynomial
float q = polynomial(b[i], 3.21, -6.54, ...);
a[i] = p/q;
}
Pour log()
sur la plage de la mantisse, un rapport de deux polynômes d'ordre N a beaucoup moins d'erreur qu'un seul polynôme avec 2N coefficients, et l'évaluation de 2 en parallèle vous donne un certain parallélisme de niveau instruction dans un seul boucle le corps au lieu d'une chaîne de dépôt massivement longue, ce qui rend les choses beaucoup plus faciles à exécuter dans le désordre.
Dans ce cas, nous n'engendrons pas de goulot d'étranglement sur la latence de division, car l'exécution dans le désordre peut conserver plusieurs itérations de la boucle sur les tableaux en vol.
Nous ne goulet d'étranglement sur divide throughput tant que nos polynômes sont assez grands pour que nous ayons seulement une division pour 10 instructions FMA environ. (Et dans un vrai cas d'utilisation de log()
, il y a beaucoup de travail pour extraire l'exposant/la mantisse et pour combiner à nouveau les choses, donc il y a encore plus de travail à faire entre les divisions.)
rcpps
x86 a une instruction réciproque approximative ( rcpps
), qui ne vous donne que 12 bits de précision. (AVX512F a 14 bits et AVX512ER a 28 bits.)
Vous pouvez l'utiliser pour faire x / y = x * approx_recip(y)
sans utiliser une instruction de division réelle. (rcpps
itsef est assez rapide; généralement un peu plus lent que la multiplication. Il utilise une recherche de table depuis une table interne au CPU. Le matériel du diviseur peut utiliser la même table comme point de départ.)
Dans la plupart des cas, x * rcpps(y)
est trop imprécis et une itération de Newton-Raphson pour doubler la précision est requise. Mais cela vous coûte 2 multiplications et 2 FMA , et a une latence à peu près aussi élevée qu'une instruction de division réelle. Si tout que vous faites est une division, cela peut être une victoire de débit. (Mais vous devriez éviter ce type de boucle en premier lieu si vous le pouvez, peut-être en faisant la division dans le cadre d'une autre boucle qui fait un autre travail.)
Mais si vous utilisez la division dans le cadre d'une fonction plus complexe, le rcpps
lui-même + le mul supplémentaire + FMA accélère généralement la division avec une instruction divps
, sauf sur les processeurs avec débit divps
très faible.
(Par exemple, Knight's Landing, voir ci-dessous. KNL prend en charge AVX512ER , donc pour les vecteurs float
le résultat VRCP28PS
Est déjà suffisamment précis pour se multiplier sans une itération de Newton-Raphson. float
la taille de la mantisse n'est que de 24 bits.)
Contrairement à toutes les autres opérations ALU, la latence/le débit de division dépend des données de certains processeurs. Encore une fois, c'est parce que c'est si lent et pas complètement canalisé. La planification dans le désordre est plus facile avec des latences fixes, car elle évite les conflits de réécriture (lorsque le même port d'exécution essaie de produire 2 résultats dans le même cycle, par exemple en exécutant une instruction à 3 cycles puis deux opérations à 1 cycle) .
Généralement, les cas les plus rapides sont lorsque le diviseur est un nombre "rond" comme 2.0
Ou 0.5
(C'est-à-dire que la représentation base2 float
a beaucoup de zéros à droite dans la mantisse).
float
latence (cycles) /débit (cycles par instruction, fonctionnant exactement dos à dos avec des entrées indépendantes):
scalar & 128b vector 256b AVX vector
divss | mulss
divps xmm | mulps vdivps ymm | vmulps ymm
Nehalem 7-14 / 7-14 | 5 / 1 (No AVX)
Sandybridge 10-14 / 10-14 | 5 / 1 21-29 / 20-28 (3 uops) | 5 / 1
Haswell 10-13 / 7 | 5 / 0.5 18-21 / 14 (3 uops) | 5 / 0.5
Skylake 11 / 3 | 4 / 0.5 11 / 5 (1 uop) | 4 / 0.5
Piledriver 9-24 / 5-10 | 5-6 / 0.5 9-24 / 9-20 (2 uops) | 5-6 / 1 (2 uops)
Ryzen 10 / 3 | 3 / 0.5 10 / 6 (2 uops) | 3 / 1 (2 uops)
Low-power CPUs:
Jaguar(scalar) 14 / 14 | 2 / 1
Jaguar 19 / 19 | 2 / 1 38 / 38 (2 uops) | 2 / 2 (2 uops)
Silvermont(scalar) 19 / 17 | 4 / 1
Silvermont 39 / 39 (6 uops) | 5 / 2 (No AVX)
KNL(scalar) 27 / 17 (3 uops) | 6 / 0.5
KNL 32 / 20 (18uops) | 6 / 0.5 32 / 32 (18 uops) | 6 / 0.5 (AVX and AVX512)
double
latence (cycles) /débit (cycles par instruction):
scalar & 128b vector 256b AVX vector
divsd | mulsd
divpd xmm | mulpd vdivpd ymm | vmulpd ymm
Nehalem 7-22 / 7-22 | 5 / 1 (No AVX)
Sandybridge 10-22 / 10-22 | 5 / 1 21-45 / 20-44 (3 uops) | 5 / 1
Haswell 10-20 / 8-14 | 5 / 0.5 19-35 / 16-28 (3 uops) | 5 / 0.5
Skylake 13-14 / 4 | 4 / 0.5 13-14 / 8 (1 uop) | 4 / 0.5
Piledriver 9-27 / 5-10 | 5-6 / 1 9-27 / 9-18 (2 uops) | 5-6 / 1 (2 uops)
Ryzen 8-13 / 4-5 | 4 / 0.5 8-13 / 8-9 (2 uops) | 4 / 1 (2 uops)
Low power CPUs:
Jaguar 19 / 19 | 4 / 2 38 / 38 (2 uops) | 4 / 2 (2 uops)
Silvermont(scalar) 34 / 32 | 5 / 2
Silvermont 69 / 69 (6 uops) | 5 / 2 (No AVX)
KNL(scalar) 42 / 42 (3 uops) | 6 / 0.5 (Yes, Agner really lists scalar as slower than packed, but fewer uops)
KNL 32 / 20 (18uops) | 6 / 0.5 32 / 32 (18 uops) | 6 / 0.5 (AVX and AVX512)
Ivybridge et Broadwell sont également différents, mais je voulais garder la table petite. (Core2 (avant Nehalem) a de meilleures performances de diviseur, mais ses vitesses d'horloge maximales étaient inférieures.)
Atom, Silvermont et même Knight's Landing (Xeon Phi basé sur Silvermont) ont des performances de division exceptionnellement faibles , et même un vecteur 128b est plus lent que scalaire. Le processeur Jaguar AMD basse consommation (utilisé sur certaines consoles) est similaire. Un séparateur haute performance prend beaucoup de place dans la matrice. Xeon Phi a une faible puissance par cœur, et le fait d'emballer beaucoup de cœurs sur un dé lui confère des contraintes de zone de dé plus strictes que Skylake-AVX512. Il semble que l'AVX512ER rcp28ps
/pd
soit ce que vous êtes "censé" utiliser sur KNL.
(Voir ce résultat InstLatx64 pour Skylake-AVX512 alias Skylake-X. Numéros pour vdivps zmm
: 18c/10c, donc la moitié du débit de ymm
.)
Les longues chaînes de latence deviennent un problème lorsqu'elles sont portées en boucle, ou lorsqu'elles sont si longues qu'elles empêchent l'exécution dans le désordre de trouver un parallélisme avec d'autres travaux indépendants.
Note de bas de page 1: comment j'ai composé ces ratios de performance div vs mul:
La division FP par rapport aux ratios de performances multiples est encore pire que celle des processeurs basse consommation comme Silvermont et Jaguar, et même dans Xeon Phi (KNL, où vous devriez utiliser AVX512ER).
Rapports de débit de division/multiplication réels pour scalaire (non vectorisé) double
: 8 sur Ryzen et Skylake avec leurs diviseurs renforcés, mais 16-28 sur Haswell (dépendant des données, et plus probablement vers la fin du cycle 28 sauf si vos diviseurs sont des nombres ronds). Ces processeurs modernes ont des diviseurs très puissants, mais leur débit multiplicateur à 2 par horloge le fait disparaître. (Encore plus lorsque votre code peut vectoriser automatiquement avec des vecteurs AVX 256b). Notez également qu'avec les bonnes options de compilation, ces débits multipliés s'appliquent également à FMA.
Numéros de http://agner.org/optimize/ tables d'instructions pour Intel Haswell/Skylake et AMD Ryzen, pour SSE scalaire (non compris x87 fmul
/fdiv
) et pour les vecteurs SIMD AVX 256b de float
ou double
. Voir aussi le wiki de balises x86 .
Oui. Chaque FPU que je connais effectue des multiplications beaucoup plus rapidement que les divisions.
Cependant, les PC modernes sont très rapides. Ils contiennent également des architectures de pipeline qui peuvent rendre la différence négligeable dans de nombreuses circonstances. Pour couronner le tout, tout compilateur décent effectuera l'opération de division que vous avez montrée au moment de la compilation avec les optimisations activées. Pour votre exemple mis à jour, tout compilateur décent effectuerait cette transformation lui-même.
Donc, généralement vous devriez vous soucier de rendre votre code lisible, et laissez le compilateur se soucier de le rendre rapide. Ce n'est que si vous avez un problème de vitesse mesurée avec cette ligne que vous devez vous soucier de pervertir votre code par souci de vitesse. Les compilateurs sont bien conscients de ce qui est plus rapide que ce qui se trouve sur leur CPU et sont généralement de bien meilleurs optimiseurs que vous ne pouvez jamais espérer.
Pensez à ce qui est requis pour la multiplication de deux nombres à n bits. Avec la méthode la plus simple, vous prenez un nombre x et vous déplacez à plusieurs reprises et vous l'ajoutez conditionnellement à un accumulateur (basé sur un bit de l'autre nombre y). Après n ajouts, vous avez terminé. Votre résultat tient dans 2n bits.
Pour la division, vous commencez avec x de 2n bits et y de n bits, vous voulez calculer x/y. La méthode la plus simple est la division longue, mais en binaire. À chaque étape, vous faites une comparaison et une soustraction pour obtenir un bit de plus du quotient. Cela vous prend n étapes.
Quelques différences: chaque étape de la multiplication n'a besoin que de regarder 1 bit; chaque étape de la division doit regarder n bits pendant la comparaison. Chaque étape de la multiplication est indépendante de toutes les autres étapes (peu importe l'ordre dans lequel vous ajoutez les produits partiels); pour la division, chaque étape dépend de l'étape précédente. C'est une grosse affaire de matériel. Si les choses peuvent être faites indépendamment, elles peuvent se produire en même temps dans un cycle d'horloge.
Newton rhapson résout la division entière en O(M(n)) complexité via une apploximation d'algèbre linéaire. Plus rapide que la complexité O (n * n) sinon.
Dans le code La méthode contient 10mults 9adds 2bitwiseshifts.
Cela explique pourquoi une division est à peu près 12 fois plus de tics de CPU qu'une multiplication.
La réponse dépend de la plateforme pour laquelle vous programmez.
Par exemple, faire beaucoup de multiplication sur un tableau sur x86 devrait être beaucoup plus rapide que faire la division, car le compilateur doit créer le code assembleur qui utilise les instructions SIMD. Puisqu'il n'y a pas de division dans les instructions SIMD, alors vous verriez de grandes améliorations en utilisant la multiplication puis la division.