web-dev-qa-db-fra.com

Pourquoi l'instruction de boucle est-elle lente? Intel n'aurait-il pas pu l'implémenter efficacement?

LOOP ( entrée manuelle de la référence Intel ) décrémente ecx/rcx, puis saute si non nul . C'est lent, mais Intel n'aurait-il pas pu le faire à bon marché? dec/jnz déjà macro-fusibles en un seul uop sur la famille Sandybridge; la seule différence étant que cela définit des drapeaux.

loop sur diverses microarchitectures, à partir de tableaux d'instructions d'Agner Fog :

  • K8/K10: 7 m-ops
  • Famille Bulldozer/Ryzen : 1 m-op (même coût que le test et la branche macro-fusionnés, ou jecxz)

  • P4: 4 uops (identique à jecxz)

  • P6 (PII/PIII): 8 uops
  • Pentium M, Core2: 11 uops
  • Nehalem: 6 uops. (11 pour loope/loopne). Débit = 4c (loop) ou 7c (loope/ne).
  • Famille SnB : 7 uops. (11 pour loope/loopne). Débit = un par 5 cycles , autant de goulot d'étranglement que de garder votre compteur de boucles en mémoire! jecxz n'est que de 2 uops avec le même débit que le régulier jcc
  • Silvermont: 7 uops
  • AMD Jaguar (basse puissance): 8 uops, 5c de débit
  • Via Nano3000: 2 uops

Les décodeurs ne pouvaient-ils pas simplement décoder comme lea rcx, [rcx-1]/jrcxz? Ce serait 3 uops. Au moins, ce serait le cas sans préfixe de taille d'adresse, sinon il doit utiliser ecx et tronquer RIP à EIP si le saut est effectué; peut-être que le choix étrange de la taille de l'adresse contrôlant la largeur du décrément explique les nombreux uops?

Ou mieux, il suffit de le décoder comme un dec-and-branch fusionné qui ne définit pas d'indicateurs? dec ecx/jnz sur SnB décode en un seul uop (qui définit des drapeaux).

Je sais que le vrai code ne l'utilise pas (car il est lent depuis au moins P5 ou quelque chose), mais AMD a décidé que cela valait la peine de le rendre rapide pour Bulldozer. Probablement parce que c'était facile.


  • Serait-il facile pour l'uarque de la famille SnB d'avoir rapidement loop? Si oui, pourquoi pas? Sinon, pourquoi est-ce difficile? Beaucoup de transistors décodeurs? Ou des bits supplémentaires dans un uop dec & branch fusionné pour enregistrer qu'il ne définit pas d'indicateurs? Que pourraient faire ces 7 uops? C'est une instruction vraiment simple.

  • Quelle est la particularité du Bulldozer qui a rendu un loop rapide facile/en vaut la peine? Ou AMD a-t-il gaspillé un tas de transistors pour rendre loop rapide? Si c'est le cas, quelqu'un a probablement pensé que c'était une bonne idée.


Si loop était rapide , ce serait parfait pour BigInteger arbitraire-précision adc boucles, pour éviter les blocages partiels/ralentissements (voir mes commentaires sur ma réponse), ou tout autre cas où vous souhaitez boucler sans toucher aux drapeaux. Il a également un avantage mineur sur la taille du code par rapport à dec/jnz. (Et dec/jnz uniquement des macro-fusibles sur la famille SnB).

Sur les processeurs modernes où dec/jnz est correct dans une boucle ADC, loop serait toujours bien pour les boucles ADCX/ADOX (pour préserver OF).

Si loop avait été rapide, les compilateurs l'auraient déjà utilisé comme optimisation de judas pour la taille du code + la vitesse sur les CPU sans macro-fusion.


Cela ne m'empêcherait pas de m'énerver à toutes les questions avec un mauvais code 16 bits qui utilise loop pour chaque boucle, même quand ils ont également besoin d'un autre compteur à l'intérieur de la boucle. Mais au moins, ce ne serait pas comme mauvais.

50
Peter Cordes

Maintenant que j'ai googlé après avoir écrit ma question , il se trouve que c'est un double exact de l'une sur arche comp. , qui est venu tout de suite. Je m'attendais à ce qu'il soit difficile de google (beaucoup de hits "pourquoi ma boucle est-elle lente"), mais mon premier essai (why is the x86 loop instruction slow) a obtenu des résultats.

Ce n'est pas une bonne ou complète réponse.

Ce sera peut-être le meilleur que nous obtiendrons, et cela devra suffire à moins que quelqu'un puisse faire la lumière sur cela. Je n'ai pas voulu écrire ceci comme un message de réponse à ma propre question.


Bons messages avec différentes théories dans ce fil:

Robert

LA BOUCLE est devenue lente sur certaines des premières machines (environ 486) lorsque des pipelines importants ont commencé à se produire, et l'exécution efficace de toutes les instructions, sauf les plus simples, dans le pipeline était technologiquement impossible. La boucle a donc été lente pendant plusieurs générations. Donc, personne ne l'a utilisé. Donc, quand il est devenu possible de l'accélérer, il n'y avait pas vraiment d'incitation à le faire, car personne ne l'utilisait réellement.


Anton Ertl :

IIRC LOOP a été utilisé dans certains logiciels pour chronométrer les boucles; il y avait des logiciels (importants) qui ne fonctionnaient pas sur les processeurs où la boucle était trop rapide (c'était au début des années 90 environ). Les fabricants de CPU ont donc appris à ralentir la boucle.


(Paul, et n'importe qui d'autre: vous êtes invités à publier à nouveau votre propre écriture comme votre propre réponse. Je vais la supprimer de ma réponse et voter pour la vôtre.)

@Paul A. Clayton (occasionnel affiche SO et gars de l'architecture CPU) a deviné comment utiliser autant d'uops . (Cela ressemble à loope/ne qui vérifie à la fois le compteur et ZF):

Je pourrais imaginer une version 6 µop peut-être sensée:

virtual_cc = cc; 
temp = test (cc); 
rCX = rCX - temp; // also setting cc 
cc = temp & cc; // assumes branch handling is not 
       // substantially changed for the sake of LOOP 
branch 
cc = virtual_cc 

(Notez que c'est 6 uops, pas 11 de SnB pour LOOPE/LOOPNE, et c'est une supposition totale n'essayant même pas de prendre en compte tout ce qui est connu des compteurs de perf SnB.)

Puis Paul a dit:

Je conviens qu'une séquence plus courte devrait être possible, mais j'essayais de penser à une séquence gonflée qui pourrait avoir un sens si des ajustements microarchitecturaux minimes étaient autorisés.

résumé: Les concepteurs voulaient que loop soit pris en charge uniquement via le microcode, sans aucun ajustement du matériel proprement dit.

Si une instruction inutile et uniquement compatible est remise aux développeurs de microcodes, ils pourraient raisonnablement ne pas être capables ou désireux de suggérer des modifications mineures à la microarchitecture interne pour améliorer une telle instruction. Non seulement ils préféreraient utiliser leur "capital de suggestion de changement" de manière plus productive, mais la suggestion d'un changement pour un cas inutile réduirait la crédibilité des autres suggestions.

(Mon avis: Intel est probablement encore en train de ralentir exprès, et n'a pas pris la peine de réécrire son microcode pour cela longtemps . CPU modernes sont probablement trop rapides pour que quoi que ce soit utilisant loop de manière naïve pour fonctionner correctement.)

... Paul continue:

Les architectes derrière Nano ont peut-être trouvé que le boîtier spécial de LOOP simplifiait leur conception en termes de surface ou de puissance. Ou ils peuvent avoir été incités par des utilisateurs intégrés à fournir une implémentation rapide (pour des avantages de densité de code). Ce ne sont que [~ # ~] sauvages [~ # ~] suppositions.

Si l'optimisation de LOOP est tombée hors d'autres optimisations (comme la fusion de comparer et de branche), il pourrait être plus facile de Tweak LOOP dans une instruction de chemin rapide que de le gérer en microcode même si les performances de LOOP étaient sans importance.

Je soupçonne que de telles décisions sont basées sur des détails spécifiques de la mise en œuvre. Les informations sur ces détails ne semblent pas être généralement disponibles et leur interprétation dépasserait le niveau de compétence de la plupart des gens. (Je ne suis pas un concepteur de matériel - et je n'en ai jamais joué à la télévision ou séjourné dans un Holiday Inn Express. :-)


Le fil est ensuite allé hors sujet dans le domaine d'AMD, nous donnant une chance unique de nettoyer la cruauté dans le codage des instructions x86. Il est difficile de les blâmer, car chaque changement est un cas où les décodeurs ne peuvent pas partager les transistors. Et avant qu'Intel n'adopte le x86-64, il n'était même pas clair que cela se reproduirait. AMD ne voulait pas surcharger leurs processeurs avec du matériel que personne n'utilisait si AMD64 ne s'en sortait pas.

Mais encore, il y a tellement de petites choses: setcc pourrait avoir changé en 32 bits. (Habituellement, vous devez utiliser xor-zero/test/setcc pour éviter les fausses dépendances, ou parce que vous avez besoin d'un reg étendu à zéro). Shift pourrait avoir des drapeaux écrits de manière inconditionnelle, même avec un décompte de décalage nul (en supprimant la dépendance des données d'entrée sur les eflags pour le décalage à nombre variable pour l'exécution OOO). La dernière fois que j'ai tapé cette liste de bêtes noires, je pense qu'il y en avait une troisième ... Oh oui, bt/bts etc. avec des opérandes de mémoire dont l'adresse dépend des bits supérieurs de l'index (chaîne de bits, pas seulement un bit dans un mot machine).

Les instructions bts sont très utiles pour les trucs de champs binaires, et sont plus lentes qu'elles ne devraient l'être donc vous voulez presque toujours charger dans un registre et ensuite l'utiliser. (Il est généralement plus rapide de déplacer/masquer pour obtenir une adresse vous-même, au lieu d'utiliser 10 uop ​​bts [mem], reg sur Skylake, mais cela prend des instructions supplémentaires. Cela avait donc du sens sur 386, mais pas sur K8). La manipulation atomique des bits doit utiliser la forme memory-dest, mais la version locked a quand même besoin de beaucoup d'ups. Il est toujours plus lent que s'il ne pouvait pas accéder à l'extérieur du dword sur lequel il fonctionne.

26
Peter Cordes

En 1988, un collègue d'IBM Glenn Henry venait de monter à bord chez Dell, qui comptait quelques centaines d'employés à l'époque, et au cours de son premier mois, il a donné une conférence technique sur 386 internes. Un certain nombre d'entre nous, les programmeurs du BIOS, se demandaient pourquoi LOOP était plus lent que DEC/JNZ, alors pendant la section questions/réponses, quelqu'un a posé la question.

Sa réponse avait du sens. Cela avait à voir avec la pagination.

LOOP se compose de deux parties: décrémenter CX, puis sauter si CX n'est pas nul. La première partie ne peut pas provoquer d'exception de processeur, tandis que la partie de saut le peut. D'une part, vous pouvez sauter (ou passer) à une adresse en dehors des limites de segment, provoquant un SEGFAULT. Pour deux, vous pouvez passer à une page qui est permutée.

Un SEGFAULT indique généralement la fin d'un processus, mais les défauts de page sont différents. Lorsqu'une erreur de page se produit, le processeur lève une exception et le système d'exploitation fait le ménage pour permuter la page du disque dans la RAM. Après cela, il redémarre l'instruction qui a provoqué le défaut.

Redémarrer signifie restaurer l'état du processus à ce qu'il était juste avant l'instruction incriminée. Dans le cas de l'instruction LOOP en particulier, cela signifiait restaurer la valeur du registre CX. On pourrait penser que vous pouvez simplement ajouter 1 à CX, car nous savons que CX a été décrémenté, mais apparemment, ce n'est pas si simple. Par exemple, consultez ceci erratum d'Intel :

Les violations de protection impliquées indiquent généralement un bogue logiciel probable et un redémarrage n'est pas souhaité si l'une de ces violations se produit. Dans un système en mode protégé 80286 avec des états d'attente pendant tous les cycles de bus, lorsque certaines violations de protection sont détectées par le composant 80286 et que le composant transfère le contrôle à la routine de gestion des exceptions, le contenu du registre CX peut ne pas être fiable. (La modification du contenu du CX dépend de l'activité du bus au moment où le microcode interne détecte la violation de protection.)

Pour être sûr, ils devaient enregistrer la valeur de CX à chaque itération d'une instruction LOOP, afin de la restaurer de manière fiable si nécessaire.

C'est ce fardeau supplémentaire lié à la sauvegarde de CX qui a rendu LOOP si lent.

Intel, comme tout le monde à l'époque, devenait de plus en plus risqué. Les anciennes instructions du CISC (LOOP, ENTER, LEAVE, BOUND) étaient progressivement supprimées. Nous les utilisions toujours dans un assemblage codé à la main, mais les compilateurs les ignoraient complètement.

14
I. J. Kennedy

Veuillez consulter l'article de Nice par Abrash, Michael, publié dans Dr.Dobb's Journal March 1991 v16 n3 p16 (8): http://archive.gamedev.net/archive/reference/articles/article369.html =

Le résumé de l'article est le suivant:

L'optimisation du code pour les microprocesseurs 8088, 80286, 80386 et 80486 est difficile car les puces utilisent des architectures de mémoire et des temps d'exécution d'instructions sensiblement différents. Le code ne peut pas être optimisé pour la famille 80x86; le code doit plutôt être conçu pour produire de bonnes performances sur une gamme de systèmes ou optimisé pour des combinaisons particulières de processeurs et de mémoire. Les programmeurs doivent éviter les instructions inhabituelles prises en charge par le 8088, qui ont perdu leurs performances Edge dans les puces suivantes. Les instructions de chaîne doivent être utilisées mais non fiables. Les registres doivent être utilisés plutôt que les opérations de mémoire. La ramification est également lente pour les quatre processeurs. Les accès à la mémoire doivent être alignés pour améliorer les performances. Généralement, l'optimisation d'un 80486 nécessite exactement les étapes opposées à l'optimisation d'un 8088.

Par "instructions inhabituelles soutenues par le 8088", l'auteur veut aussi dire "boucle":

Tout programmeur 8088 remplacerait instinctivement: DEC CX JNZ LOOPTOP par: LOOP LOOPTOP car LOOP est nettement plus rapide sur le 8088. LOOP est également plus rapide sur le 286. Sur le 386, cependant, LOOP est en réalité deux cycles plus lent que DEC/JNZ. Le pendule oscille encore plus loin sur le 486, où LOOP est environ deux fois plus lent que DEC/JNZ - et, rappelez-vous, nous parlons de ce qui était à l'origine peut-être l'optimisation la plus évidente de l'ensemble des instructions 80x86.

Ceci est un très bon article, et je le recommande vivement. Même s'il a été publié en 1991, il est étonnamment très pertinent aujourd'hui.

Mais cet article ne donne que des conseils, il encourage à tester la vitesse d'exécution et à choisir des variantes plus rapides. Cela n'explique pas POURQUOI certaines commandes deviennent très lentes, donc cela ne répond pas complètement à votre question.

La réponse est que les processeurs antérieurs, comme 80386 (sorti en 1985) et avant, exécutaient les instructions une par une, séquentiellement.

Les processeurs ultérieurs ont commencé à utiliser le pipelining d'instructions - initialement, simple, pour 804086, et, enfin, Pentium Pro (sorti en 1995) a introduit un pipeline interne radicalement différent, l'appelant le noyau Out Of Order (OOO) où les instructions ont été transformées en petits fragments d'opérations appelées micro-opérations ou µops, puis toutes les micro-opérations d'instructions différentes ont été placées dans un large pool de micro-opérations où elles étaient censées s'exécuter simultanément tant qu'elles ne dépendent pas les unes des autres. Ce principe de pipeline OOO est toujours utilisé, presque inchangé, sur les processeurs modernes. Vous pouvez trouver plus d'informations sur le pipeline d'instructions dans cet article brillant: https://www.gamedev.net/resources/_/technical/general-programming/a-journey-through-the-cpu-pipeline-r3115

Afin de simplifier la conception des puces, Intel a décidé de construire des processeurs de telle manière qu'une instruction se transforme en micro-op de manière très efficace, tandis que d'autres ne le sont pas.

Une conversion efficace des instructions en micro-opérations nécessite plus de transistors, Intel a donc décidé d'économiser sur les transistors au prix d'un décodage et d'une exécution plus lents de certaines instructions "complexes" ou "rarement utilisées".

Par exemple, le "Intel® Architecture Optimization Reference Manual" http://download.intel.com/design/PentiumII/manuals/24512701.pdf mentionne ce qui suit: "Évitez d'utiliser des instructions complexes (par exemple , entrée, sortie ou boucle) qui ont généralement plus de quatre µops et nécessitent plusieurs cycles de décodage. Utilisez plutôt des séquences d'instructions simples. "

Ainsi, Intel a en quelque sorte décidé que l'instruction de "boucle" est "complexe" et, depuis lors, elle est devenue très lente. Cependant, il n'y a pas de référence officielle d'Intel sur la ventilation des instructions: combien de micro-opérations chaque instruction produit et combien de cycles sont nécessaires pour la décoder.

Vous pouvez également lire sur le moteur d'exécution hors service dans le "Intel® 64 and IA-32 Architectures Optimization Reference Manual" http://www.intel.com/content/dam/www/public/ fr/documents/manuels/64-ia-32-architectures-optimisation-manual.pdf section 2.1.2.

6
Maxim Masiutin