Les guides d'optimisation d'Agner Fog sont excellents. Il a des guides, des tableaux de synchronisation des instructions et des documents sur la microarchitecture de toutes les conceptions récentes de CPU x86 (remontant jusqu'à Intel Pentium). Voir aussi quelques autres ressources liées depuis /programming//tags/x86/info
Juste pour le plaisir, je répondrai à certaines des questions (chiffres des processeurs Intel récents). Le choix des opérations n'est pas le principal facteur d'optimisation du code (sauf si vous pouvez éviter la division.)
Une seule multiplication est-elle plus lente sur le processeur qu'un ajout?
Oui (sauf si c'est par une puissance de 2). (3-4x la latence, avec seulement un par débit d'horloge sur Intel.) Ne vous éloignez pas pour l'éviter, car c'est aussi rapide que 2 ou 3 ajouts.
Quelles sont exactement les caractéristiques de vitesse des opcodes mathématiques et de flux de contrôle de base?
Consultez les tableaux d'instructions et le guide de microarchitecture d'Agner Fog si vous voulez savoir exactement : P. Soyez prudent avec les sauts conditionnels. Les sauts inconditionnels (comme les appels de fonction) ont une petite surcharge, mais pas beaucoup.
Si deux opcodes prennent le même nombre de cycles à exécuter, alors les deux peuvent être utilisés de manière interchangeable sans aucun gain / perte de performances?
Non, ils pourraient rivaliser pour le même port d'exécution qu'autre chose, ou ils pourraient ne pas le faire. Cela dépend des autres chaînes de dépendance sur lesquelles le processeur peut travailler en parallèle. (En pratique, il n'y a généralement pas de décision utile à prendre. Il arrive parfois que vous puissiez utiliser un décalage vectoriel ou un shuffle vectoriel, qui s'exécutent sur différents ports sur les processeurs Intel. Mais le décalage par octets de l'ensemble du registre ( PSLLDQ
etc.) fonctionne dans l'unité de lecture aléatoire.)
Tout autre détail technique que vous pouvez partager concernant les performances du processeur x86 est apprécié
Les documents microarch d'Agner Fog décrivent les pipelines des processeurs Intel et AMD avec suffisamment de détails pour déterminer exactement combien de cycles une boucle devrait prendre par itération, et si le goulot d'étranglement est le débit uop, une chaîne de dépendance ou la contention pour un port d'exécution. Voir certaines de mes réponses sur StackOverflow, comme celle-ci ou celle-ci .
En outre, http://www.realworldtech.com/haswell-cpu/ (et similaire pour les conceptions antérieures) est amusant à lire si vous aimez la conception de CPU.
Voici votre liste, triée pour un processeur Haswell, basée sur mes meilleures estimations. Ce n'est pas vraiment une façon utile de penser aux choses pour autre chose que le réglage d'une boucle asm. Les effets de prédiction de cache / branche dominent généralement, alors écrivez votre code pour avoir de bons modèles. Les nombres sont très ondulants et essaient de tenir compte d'une latence élevée, même si le débit n'est pas un problème, ou de générer plus d'ups qui obstruent le tuyau pour que d'autres choses se produisent en parallèle. Esp. les numéros de cache / branche sont très composés. La latence est importante pour les dépendances transportées en boucle, le débit est important lorsque chaque itération est indépendante.
TL: DR ces chiffres sont composés en fonction de ce que j'imagine pour un cas d'utilisation "typique", en ce qui concerne les compromis entre la latence, les goulots d'étranglement des ports d'exécution et le débit frontal (ou les blocages pour des choses comme les échecs de branche ). Veuillez ne pas utiliser ces chiffres pour tout type d'analyse de performance sérieuse .
- 0,5 à 1 au niveau du bit / addition entière / soustraction /
décalage et rotation (nombre de const à la compilation) /
versions vectorielles de tous ces éléments (1 à 4 par cycle de débit, 1 cycle de latence)
- 1 vecteur min, max, comparer-égal, comparer-plus (pour créer un masque)
- 1.5 mélange de vecteurs. Haswell et les plus récents n'ont qu'un seul port de lecture aléatoire, et il me semble qu'il est courant d'avoir besoin de beaucoup de lecture aléatoire si vous en avez besoin, donc je le pondère légèrement plus haut pour encourager à penser à utiliser moins de lecture aléatoire. Ils ne sont pas gratuits, surtout. si vous avez besoin d'un masque de contrôle pshufb de la mémoire.
- 1.5 chargement / stockage (accès au cache L1. Débit supérieur à la latence)
- 1,75 Multiplication de nombres entiers (latence 3c / une par sortie 1c sur Intel, lat 4c sur AMD et une seule par sortie 2c). Les petites constantes sont encore moins chères en utilisant LEA et / ou ADD / SUB / shift . Mais bien sûr, les constantes au moment de la compilation sont toujours bonnes et peuvent souvent être optimisées pour d'autres choses. (Et la multiplication dans une boucle peut souvent être réduite par le compilateur
tmp += 7
dans une boucle au lieu de tmp = i*7
)
- 1.75 quelques shuffle vectoriels 256b (latence supplémentaire sur les insns qui peuvent déplacer des données entre 128b voies d'un vecteur AVX). (Ou 3 à 7 sur Ryzen où les shuffles de franchissement de voie ont besoin de beaucoup plus d'ups)
- 2 fp add / sub (et versions vectorielles de celui-ci) (1 ou 2 par débit de cycle, latence de 3 à 5 cycles). Peut être lent si vous goulot d'étranglement sur la latence, par exemple en sommant un tableau avec seulement 1
sum
variable. (Je pourrais peser cela et fp mul aussi bas que 1 ou aussi haut que 5 selon le cas d'utilisation).
- 2 vecteurs fp mul ou FMA. (x * y + z est aussi bon marché qu'un mul ou un add si vous compilez avec le support FMA activé).
- 2 insertion / extraction de registres à usage général dans des éléments vectoriels (
_mm_insert_epi8
, etc.)
- 2.25 vector int mul (éléments 16 bits ou pmaddubsw faisant 8 * 8 -> 16 bits). Moins cher sur Skylake, avec un meilleur débit que le scalaire mul
- 2.25 décalage / rotation par nombre variable (latence 2c, un par débit 2c sur Intel, plus rapide sur AMD ou avec BMI2)
- 2.5 Comparaison sans branchement (
y = x ? a : b
, ou y = x >= 0
) ( test / setcc
ou cmov
)
- 3 conversion int-> float
- 3 Flux de contrôle parfaitement prévu (branchement prévu, appel, retour).
- 4 vecteurs int mul (éléments 32 bits) (2 uops, latence 10c sur Haswell)
- 4 division entière ou
%
par une constante de temps de compilation (non-puissance de 2).
- 7 opérations horizontales vectorielles (par exemple,
PHADD
ajout de valeurs dans un vecteur)
- 11 (vector) FP Division (latence 10-13c, une par débit 7c ou pire). (Peut être bon marché si utilisé rarement, mais le débit est 6 à 40 fois pire que FP mul)
- 13? Flux de contrôle (branche mal prédite, peut-être 75% prévisible)
- 13 division int ( oui vraiment , c'est plus lent que la division FP et ne peut pas vectoriser). (Notez que les compilateurs divisent par une constante en utilisant mul / shift / add avec une constante magique , et div / mod par des puissances de 2 est très bon marché.)
- 16 (vecteur) FP sqrt
- 25? charge (accès au cache L3). (les magasins cache-miss sont moins chers que les charges.)
- 50? FP trig / exp / log. Si vous avez besoin de beaucoup d'exp / log et n'avez pas besoin d'une précision totale, vous pouvez échanger la précision contre la vitesse avec un polynôme plus court et / ou une table. Vous pouvez également vectoriser SIMD.
- 50-80? branche toujours imprévue, coûtant 15 à 20 cycles
- 200-400? charger / stocker (cache manquant)
- 3000 ??? lire la page à partir du fichier (hit du cache du disque du système d'exploitation) (composition des nombres ici)
- 20000 ??? page de lecture du disque (échec du cache du disque du système d'exploitation, SSD rapide) (numéro entièrement composé)
J'ai totalement inventé cela sur la base de suppositions . Si quelque chose ne va pas, c'est soit parce que je pensais à un cas d'utilisation différent, soit à une erreur d'édition.
Le coût relatif des choses sur les processeurs AMD sera similaire, sauf qu'ils ont des décaleurs entiers plus rapides lorsque le nombre de décalages est variable. Les processeurs de la famille AMD Bulldozer sont bien sûr plus lents sur la plupart des codes, pour diverses raisons. (Ryzen est assez bon pour beaucoup de choses).
Gardez à l'esprit qu'il est vraiment impossible de réduire les choses à un coût unidimensionnel . Outre les erreurs de cache et les erreurs de branchement, le goulot d'étranglement dans un bloc de code peut être la latence, le débit uop total (frontend) ou le débit d'un port spécifique (port d'exécution).
Une opération "lente" comme la division FP peut être très bon marché si le code environnant maintient le CPU occupé avec d'autres travaux . (le vecteur FP div ou sqrt sont 1 uop chacun, ils ont juste une latence et un débit médiocres. Ils bloquent uniquement l'unité de division, pas le port d'exécution entier sur lequel il est activé. La division entière est de plusieurs uops.) Donc, si vous n'avez qu'une seule division FP pour chaque ~ 20 mul et ajouter, et il y a d'autres travaux à faire par le CPU (par exemple une itération de boucle indépendante), alors le "coût" de la div FP pourrait être à peu près le même qu'un FP mul. C'est probablement le meilleur exemple de quelque chose qui est à faible débit quand c'est tout ce que vous faites, mais qui se mélange très bien avec d'autres codes (lorsque la latence n'est pas un facteur), en raison du faible nombre total d'ups.
Notez que la division entière n'est pas aussi conviviale que le code environnant: Sur Haswell, c'est 9 uops, avec un par débit 8-11c, et une latence 22-29c. (La division 64 bits est beaucoup plus lente, même sur Skylake.) Ainsi, les nombres de latence et de débit sont quelque peu similaires à FP div, mais FP div n'est qu'un uop.
Pour des exemples d'analyse d'une courte séquence d'insns pour le débit, la latence et le nombre total d'ups, consultez certaines de mes réponses SO:
IDK si d'autres personnes écrivent des réponses SO incluant ce type d'analyse. J'ai beaucoup plus de facilité à trouver le mien, car je sais que je vais souvent dans ce détail et je me souviens de ce que j'ai écrit.