L'une des raisons invoquées pour connaître l'assembleur est que, à l'occasion, il peut être utilisé pour écrire du code qui sera plus performant que d'écrire ce code dans un langage de niveau supérieur, C en particulier. Cependant, j'ai également entendu dire à plusieurs reprises que bien que ce ne soit pas entièrement faux, les cas où l'assembleur peut en fait être utilisé pour générer du code plus performant sont à la fois extrêmement rares et nécessitent une connaissance et une expérience spécialisées de l'assemblage.
Cette question n'entre même pas dans le fait que les instructions d'assembleur seront spécifiques à la machine et non portables, ni aucun des autres aspects de l'assembleur. Il existe de nombreuses bonnes raisons de connaître l'assembly en plus de celui-ci, bien sûr, mais cela est censé être une question spécifique sollicitant des exemples et des données, pas un discours étendu sur l'assembleur par rapport aux langages de niveau supérieur.
Quelqu'un peut-il fournir des exemples spécifiques de cas où l'assemblage sera plus rapide qu'un code C bien écrit à l'aide d'un compilateur moderne, et pouvez-vous soutenir cette revendication avec des preuves de profilage? Je suis assez confiant que ces cas existent, mais je veux vraiment savoir exactement à quel point ces cas sont ésotériques, car cela semble être un sujet de controverse.
la source
-O3
drapeau, vous feriez probablement mieux de laisser l'optimisation au compilateur C :-)Réponses:
Voici un exemple réel: le point fixe se multiplie sur les anciens compilateurs.
Ceux-ci ne sont pas seulement utiles sur les appareils sans virgule flottante, ils brillent en termes de précision car ils vous donnent 32 bits de précision avec une erreur prévisible (float n'a que 23 bits et il est plus difficile de prédire la perte de précision). c'est-à-dire une précision absolue uniforme sur toute la plage, au lieu d' une précision relative proche de l'uniforme (
float
).Les compilateurs modernes optimisent bien cet exemple à virgule fixe, donc pour des exemples plus modernes qui nécessitent encore du code spécifique au compilateur, consultez
uint64_t
32x32 => 64 bits ne parvient pas à optimiser sur un processeur 64 bits, vous avez donc besoin d'intrinsèques ou__int128
d'un code efficace sur les systèmes 64 bits.C n'a pas d'opérateur de multiplication complète (résultat de 2 N bits à partir d'entrées N bits). La façon habituelle de l'exprimer en C est de convertir les entrées en type plus large et d'espérer que le compilateur reconnaîtra que les bits supérieurs des entrées ne sont pas intéressants:
Le problème avec ce code est que nous faisons quelque chose qui ne peut pas être directement exprimé en langage C. Nous voulons multiplier deux nombres de 32 bits et obtenir un résultat de 64 bits dont nous retournons le 32 bits du milieu. Cependant, en C, cette multiplication n'existe pas. Tout ce que vous pouvez faire est de promouvoir les entiers en 64 bits et de faire une multiplication 64 * 64 = 64.
x86 (et ARM, MIPS et autres) peuvent cependant faire la multiplication en une seule instruction. Certains compilateurs ignoraient ce fait et généraient du code qui appelle une fonction de bibliothèque d'exécution pour effectuer la multiplication. Le décalage de 16 est également souvent effectué par une routine de bibliothèque (le x86 peut également effectuer de tels décalages).
Il nous reste donc un ou deux appels de bibliothèque juste pour une multiplication. Cela a de graves conséquences. Non seulement le décalage est plus lent, les registres doivent être préservés dans les appels de fonction et cela n'aide pas non plus à aligner et à dérouler le code.
Si vous réécrivez le même code dans l'assembleur (en ligne), vous pouvez obtenir une augmentation de vitesse significative.
En plus de cela: l'utilisation d'ASM n'est pas la meilleure façon de résoudre le problème. La plupart des compilateurs vous permettent d'utiliser certaines instructions d'assembleur sous forme intrinsèque si vous ne pouvez pas les exprimer en C. Le compilateur VS.NET2008, par exemple, expose le mul 32 * 32 = 64 bits comme __emul et le décalage 64 bits comme __ll_rshift.
En utilisant intrinsèques, vous pouvez réécrire la fonction de manière à ce que le compilateur C ait une chance de comprendre ce qui se passe. Cela permet au code d'être aligné, alloué au registre, l'élimination de la sous-expression commune et la propagation constante peuvent également être effectuées. Vous obtiendrez ainsi une énorme amélioration des performances par rapport au code assembleur manuscrit.
Pour référence: Le résultat final pour le mul à virgule fixe pour le compilateur VS.NET est:
La différence de performance des divisions en virgule fixe est encore plus grande. J'ai eu des améliorations jusqu'au facteur 10 pour le code à point fixe lourd de division en écrivant quelques lignes asm.
L'utilisation de Visual C ++ 2013 donne le même code d'assembly dans les deux sens.
gcc4.1 de 2007 optimise également la version C pure. (L'explorateur du compilateur Godbolt n'a pas de versions antérieures de gcc installées, mais il est probable que même les anciennes versions de GCC pourraient le faire sans intrinsèques.)
Voir source + asm pour x86 (32 bits) et ARM sur l'explorateur du compilateur Godbolt . (Malheureusement, il n'a pas de compilateurs assez vieux pour produire du mauvais code à partir de la simple version C pure.)
Les processeurs modernes peuvent faire des choses C n'a pas d'opérateurs du tout , comme
popcnt
ou bit-scan pour trouver le premier ou le dernier bit défini . (POSIX a uneffs()
fonction, mais sa sémantique ne correspond pas à x86bsf
/bsr
. Voir https://en.wikipedia.org/wiki/Find_first_set ).Certains compilateurs peuvent parfois reconnaître une boucle qui compte le nombre de bits définis dans un entier et le compiler en une
popcnt
instruction (si activé au moment de la compilation), mais il est beaucoup plus fiable à utiliser__builtin_popcnt
dans GNU C, ou sur x86 si vous êtes seulement cibler le matériel avec SSE4.2: à_mm_popcnt_u32
partir de<immintrin.h>
.Ou en C ++, attribuez à a
std::bitset<32>
et utilisez.count()
. (Il s'agit d'un cas où le langage a trouvé un moyen d'exposer de manière portable une implémentation optimisée de popcount via la bibliothèque standard, d'une manière qui se compilera toujours en quelque chose de correct, et peut tirer parti de tout ce que la cible prend en charge.) Voir aussi https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .De même,
ntohl
peut être compilé versbswap
(échange d'octets x86 32 bits pour la conversion endian) sur certaines implémentations C qui en sont dotées.Un autre domaine majeur de l'intrinsèque ou de l'asm manuscrit est la vectorisation manuelle avec des instructions SIMD. Les compilateurs ne sont pas mauvais avec des boucles simples comme
dst[i] += src[i] * 10.0;
, mais font souvent mal ou ne se vectorisent pas du tout quand les choses deviennent plus compliquées. Par exemple, il est peu probable que vous obteniez quelque chose comme Comment implémenter atoi en utilisant SIMD? généré automatiquement par le compilateur à partir du code scalaire.la source
Il y a plusieurs années, j'enseignais à quelqu'un de programmer en C. L'exercice consistait à faire pivoter un graphique de 90 degrés. Il est revenu avec une solution qui a pris plusieurs minutes à compléter, principalement parce qu'il utilisait des multiplications et des divisions, etc.
Je lui ai montré comment reformuler le problème en utilisant des décalages de bits, et le temps de traitement est descendu à environ 30 secondes sur le compilateur non optimisant qu'il avait.
Je venais de recevoir un compilateur d'optimisation et le même code faisait pivoter le graphique en moins de 5 secondes. J'ai regardé le code d'assembly que le compilateur générait, et d'après ce que j'ai vu, j'ai décidé que mes jours d'écriture d'assembleur étaient terminés.
la source
add di,di / adc al,al / add di,di / adc ah,ah
etc. pour les huit registres 8 bits, puis répète les 8 registres, puis répète toute la procédure trois plusieurs fois, et enfin enregistrer quatre mots dans ax / bx / cx / dx. Pas question pour un assembleur de s'en approcher.À peu près à chaque fois que le compilateur voit du code à virgule flottante, une version manuscrite sera plus rapide si vous utilisez un ancien mauvais compilateur. ( Mise à jour 2019: ce n'est pas vrai en général pour les compilateurs modernes. Surtout lors de la compilation pour autre chose que x87; les compilateurs ont plus de facilité avec SSE2 ou AVX pour les calculs scalaires, ou tout non-x86 avec un jeu de registres FP plat, contrairement à x87 enregistrer la pile.)
La raison principale est que le compilateur ne peut effectuer aucune optimisation robuste. Consultez cet article de MSDN pour une discussion sur le sujet. Voici un exemple où la version d'assembly est deux fois plus rapide que la version C (compilée avec VS2K5):
Et quelques chiffres de mon PC exécutant une version par défaut * :
Par intérêt, j'ai échangé la boucle avec un dec / jnz et cela n'a fait aucune différence pour les timings - parfois plus rapides, parfois plus lents. Je suppose que l'aspect mémoire limité éclipse les autres optimisations. (NDLR: il est plus probable que le goulot d'étranglement de latence FP soit suffisant pour masquer le coût supplémentaire de
loop
. Faire deux sommations de Kahan en parallèle pour les éléments pairs / impairs, et ajouter ceux à la fin, pourrait peut-être accélérer cela par un facteur de 2. )Oups, j'exécutais une version légèrement différente du code et il sortait les chiffres dans le mauvais sens (c'est-à-dire que C était plus rapide!). Correction et mise à jour des résultats.
la source
-ffast-math
. Ils ont un niveau d'optimisation,-Ofast
qui est actuellement équivalent à-O3 -ffast-math
, mais à l'avenir peut inclure plus d'optimisations qui peuvent conduire à une génération de code incorrecte dans les cas d'angle (comme le code qui repose sur les NaN IEEE).a+b == b+a
), mais pas associatif (réorganisation des opérations, donc l'arrondi des intermédiaires est différent). re: ce code: je ne pense pas qu'un x87 non commenté et uneloop
instruction soient une démonstration très impressionnante de fast asm.loop
n'est apparemment pas un goulot d'étranglement en raison de la latence de la PF. Je ne sais pas s'il pipeline des opérations de PF ou non; x87 est difficile à lire pour les humains. Deuxfstp results
insns à la fin ne sont clairement pas optimaux. Il serait préférable de supprimer le résultat supplémentaire de la pile avec un non-stockage. Comme l'fstp st(0)
IIRC.Sans donner d'exemple spécifique ni de preuve de profileur, vous pouvez écrire un meilleur assembleur que le compilateur lorsque vous en savez plus que le compilateur.
Dans le cas général, un compilateur C moderne en sait beaucoup plus sur la façon d'optimiser le code en question: il sait comment fonctionne le pipeline du processeur, il peut essayer de réorganiser les instructions plus rapidement qu'un humain, et ainsi de suite - c'est fondamentalement la même chose que un ordinateur étant aussi bon ou meilleur que le meilleur joueur humain pour les jeux de société, etc. simplement parce qu'il peut effectuer des recherches dans l'espace problématique plus rapidement que la plupart des humains. Bien que vous puissiez théoriquement fonctionner aussi bien que l'ordinateur dans un cas spécifique, vous ne pouvez certainement pas le faire à la même vitesse, ce qui le rend irréalisable pour plus de quelques cas (c'est-à-dire que le compilateur vous surpassera très certainement si vous essayez d'écrire plusieurs routines dans l'assembleur).
D'un autre côté, il y a des cas où le compilateur n'a pas autant d'informations - je dirais principalement lorsqu'il travaille avec différentes formes de matériel externe, dont le compilateur n'a aucune connaissance. L'exemple principal étant probablement les pilotes de périphériques, où l'assembleur combiné à la connaissance intime d'un humain du matériel en question peut donner de meilleurs résultats qu'un compilateur C ne pourrait le faire.
D'autres ont mentionné des instructions spéciales, ce dont je parle dans le paragraphe ci-dessus - des instructions dont le compilateur peut avoir une connaissance limitée ou inexistante, permettant à un humain d'écrire du code plus rapidement.
la source
ocamlopt
ignore la planification des instructions sur x86 et, à la place, laisse le soin au processeur car il peut être réorganisé plus efficacement au moment de l'exécution.Dans mon travail, il y a trois raisons pour lesquelles je connais et utilise l'assemblage. Par ordre d'importance:
Débogage - J'obtiens souvent du code de bibliothèque contenant des bogues ou une documentation incomplète. Je comprends ce qu'il fait en intervenant au niveau de l'assemblage. Je dois le faire environ une fois par semaine. Je l'utilise également comme outil pour déboguer des problèmes dans lesquels mes yeux ne repèrent pas l'erreur idiomatique en C / C ++ / C #. Regarder l'assemblée passe au-delà.
Optimisation - le compilateur réussit assez bien dans l'optimisation, mais je joue dans un stade différent de la plupart. J'écris du code de traitement d'image qui commence généralement par un code qui ressemble à ceci:
la partie «faire quelque chose» se produit généralement de l'ordre de plusieurs millions de fois (c'est-à-dire entre 3 et 30). En grattant les cycles dans cette phase "faire quelque chose", les gains de performances sont énormément amplifiés. Je ne commence généralement pas par là - je commence généralement par écrire le code pour fonctionner d'abord, puis je fais de mon mieux pour refactoriser le C pour qu'il soit naturellement meilleur (meilleur algorithme, moins de charge dans la boucle, etc.). J'ai généralement besoin de lire l'assemblage pour voir ce qui se passe et j'ai rarement besoin de l'écrire. Je fais cela peut-être tous les deux ou trois mois.
faire quelque chose que la langue ne me laisse pas. Ceux-ci incluent - obtenir l'architecture du processeur et les fonctionnalités spécifiques du processeur, accéder aux drapeaux qui ne se trouvent pas dans le processeur (homme, je souhaite vraiment que C vous ait donné accès au drapeau de transport), etc. Je le fais peut-être une fois par an ou deux ans.
la source
Ce n'est que lorsque vous utilisez des jeux d'instructions spéciales que le compilateur ne prend pas en charge.
Pour maximiser la puissance de calcul d'un CPU moderne avec plusieurs pipelines et branchements prédictifs, vous devez structurer le programme d'assemblage de manière à ce qu'il soit a) presque impossible pour un humain d'écrire b) encore plus impossible à maintenir.
En outre, de meilleurs algorithmes, structures de données et gestion de la mémoire vous donneront au moins un ordre de grandeur plus de performances que les micro-optimisations que vous pouvez faire en assemblage.
la source
Bien que C soit "proche" de la manipulation de bas niveau des données 8 bits, 16 bits, 32 bits et 64 bits, il existe quelques opérations mathématiques non prises en charge par C qui peuvent souvent être exécutées avec élégance dans certaines instructions d'assemblage. ensembles:
Multiplication à virgule fixe: le produit de deux nombres à 16 bits est un nombre à 32 bits. Mais les règles de C indiquent que le produit de deux nombres 16 bits est un nombre 16 bits, et le produit de deux nombres 32 bits est un nombre 32 bits - la moitié inférieure dans les deux cas. Si vous voulez la moitié supérieure d'une multiplication 16x16 ou 32x32, vous devez jouer à des jeux avec le compilateur. La méthode générale consiste à transtyper sur une largeur de bit plus grande que nécessaire, à multiplier, à rétrograder et à rétrograder:
Dans ce cas, le compilateur peut être suffisamment intelligent pour savoir que vous essayez vraiment d'obtenir la moitié supérieure d'une multiplication 16x16 et de faire la bonne chose avec la multiplication 16x16 native de la machine. Ou cela peut être stupide et nécessiter un appel de bibliothèque pour effectuer la multiplication 32x32, ce qui est excessif car vous n'avez besoin que de 16 bits du produit - mais la norme C ne vous donne aucun moyen de vous exprimer.
Certaines opérations de décalage de bits (rotation / portage):
Ce n'est pas trop inélégant en C, mais encore une fois, à moins que le compilateur ne soit assez intelligent pour réaliser ce que vous faites, il va faire beaucoup de travail "inutile". De nombreux jeux d'instructions d'assemblage vous permettent de faire pivoter ou de décaler vers la gauche / droite avec le résultat dans le registre de report, vous pouvez donc accomplir les étapes ci-dessus dans 34 instructions: charger un pointeur au début du tableau, effacer le report et effectuer 32 8- bits à droite, en utilisant l'incrémentation automatique sur le pointeur.
Pour un autre exemple, il existe des registres à décalage à rétroaction linéaire (LFSR) qui sont élégamment exécutés en assemblage: prenez un morceau de N bits (8, 16, 32, 64, 128, etc.), déplacez le tout à droite de 1 (voir ci-dessus). algorithme), alors si le portage résultant est 1 alors vous XOR dans un motif binaire qui représente le polynôme.
Cela dit, je n'aurais pas recours à ces techniques à moins d'avoir de sérieuses contraintes de performance. Comme d'autres l'ont dit, l'assemblage est beaucoup plus difficile à documenter / déboguer / tester / maintenir que le code C: le gain de performances entraîne de sérieux coûts.
edit: 3. La détection de débordement est possible dans l'assemblage (ne peut pas vraiment le faire en C), cela rend certains algorithmes beaucoup plus faciles.
la source
Réponse courte? Quelquefois.
Techniquement, chaque abstraction a un coût et un langage de programmation est une abstraction pour le fonctionnement du CPU. C est cependant très proche. Il y a des années, je me souviens avoir ri à haute voix lorsque je me suis connecté à mon compte UNIX et que j'ai reçu le message de fortune suivant (lorsque de telles choses étaient populaires):
C'est drôle parce que c'est vrai: C est comme un langage d'assemblage portable.
Il convient de noter que le langage d'assemblage s'exécute comme vous l'écrivez. Il existe cependant un compilateur entre C et le langage d'assemblage qu'il génère et cela est extrêmement important car la vitesse à laquelle votre code C est a beaucoup à voir avec la qualité de votre compilateur.
Lorsque gcc est entré en scène, l'une des choses qui l'ont rendu si populaire, c'est qu'il était souvent bien meilleur que les compilateurs C fournis avec de nombreuses saveurs UNIX commerciales. Non seulement c'était ANSI C (aucun de ces déchets K&R C), il était plus robuste et produisait généralement un meilleur code (plus rapide). Pas toujours mais souvent.
Je vous dis tout cela parce qu'il n'y a pas de règle générale sur la vitesse de C et de l'assembleur car il n'y a pas de norme objective pour C.
De même, l'assembleur varie beaucoup selon le processeur que vous utilisez, les spécifications de votre système, le jeu d'instructions que vous utilisez, etc. Historiquement, il y avait deux familles d'architecture CPU: CISC et RISC. Le plus grand acteur du CISC était et est toujours l'architecture Intel x86 (et le jeu d'instructions). RISC a dominé le monde UNIX (MIPS6000, Alpha, Sparc et ainsi de suite). L'ICCA a remporté la bataille des cœurs et des esprits.
Quoi qu'il en soit, la sagesse populaire quand j'étais un jeune développeur était que le x86 manuscrit pouvait souvent être beaucoup plus rapide que C parce que la façon dont l'architecture fonctionnait, il avait une complexité qui bénéficiait d'un humain. RISC, d'autre part, semblait conçu pour les compilateurs, donc personne (je le savais) n'a écrit, dit l'assembleur Sparc. Je suis sûr que de telles personnes existaient, mais sans aucun doute elles sont toutes les deux devenues folles et ont été institutionnalisées maintenant.
Les ensembles d'instructions sont un point important même dans la même famille de processeurs. Certains processeurs Intel ont des extensions comme SSE à SSE4. AMD avait ses propres instructions SIMD. L'avantage d'un langage de programmation comme C était que quelqu'un pouvait écrire sa bibliothèque, donc elle était optimisée pour le processeur sur lequel vous exécutiez. Ce fut un travail difficile en assembleur.
Il y a encore des optimisations que vous pouvez faire dans un assembleur qu'aucun compilateur ne pourrait faire et un assembleur bien écrit algoirthm sera aussi rapide ou plus rapide que son équivalent C. La plus grande question est: cela en vaut-il la peine?
En fin de compte, l'assembleur était un produit de son temps et était plus populaire à une époque où les cycles CPU étaient chers. De nos jours, un processeur dont la fabrication coûte 5 à 10 $ (Intel Atom) peut faire à peu près tout ce que l'on peut souhaiter. La seule vraie raison d'écrire l'assembleur de nos jours est pour des choses de bas niveau comme certaines parties d'un système d'exploitation (même si la grande majorité du noyau Linux est écrite en C), les pilotes de périphériques, éventuellement les périphériques intégrés (bien que C tend à y dominer) aussi) et ainsi de suite. Ou tout simplement pour les coups de pied (ce qui est quelque peu masochiste).
la source
Un cas d'utilisation qui pourrait ne plus s'appliquer mais pour votre plus grand plaisir: sur l'Amiga, le processeur et les puces graphiques / audio se battraient pour accéder à une certaine zone de RAM (les 2 premiers Mo de RAM pour être spécifiques). Ainsi, lorsque vous n'aviez que 2 Mo de RAM (ou moins), afficher des graphiques complexes et jouer du son réduirait les performances du processeur.
Dans l'assembleur, vous pouvez entrelacer votre code d'une manière si intelligente que le CPU n'essaiera d'accéder à la RAM que lorsque les puces graphiques / audio sont occupées en interne (c'est-à-dire lorsque le bus est libre). Donc, en réorganisant vos instructions, en utilisant intelligemment le cache du processeur, la synchronisation du bus, vous pouvez obtenir des effets qui n'étaient tout simplement pas possibles en utilisant un langage de niveau supérieur car vous deviez chronométrer chaque commande, même insérer des NOP ici et là pour garder les divers puces hors de l'autre radar.
C'est une autre raison pour laquelle l'instruction NOP (Aucune opération - ne rien faire) du CPU peut réellement faire fonctionner votre application plus rapidement.
[EDIT] Bien sûr, la technique dépend d'une configuration matérielle spécifique. Ce qui était la principale raison pour laquelle de nombreux jeux Amiga ne pouvaient pas faire face à des processeurs plus rapides: le timing des instructions était décalé.
la source
Point un qui n'est pas la réponse.
Même si vous ne le programmez jamais, je trouve utile de connaître au moins un jeu d'instructions d'assembleur. Cela fait partie de la quête sans fin des programmeurs pour en savoir plus et donc être meilleur. Également utile lorsque vous entrez dans des frameworks, vous n'avez pas le code source et vous avez au moins une idée approximative de ce qui se passe. Il vous aide également à comprendre JavaByteCode et .Net IL car ils sont tous deux similaires à l'assembleur.
Pour répondre à la question lorsque vous avez une petite quantité de code ou une grande quantité de temps. Le plus utile pour une utilisation dans des puces intégrées, où une faible complexité des puces et une faible concurrence dans les compilateurs ciblant ces puces peuvent faire pencher la balance en faveur des humains. De plus, pour les appareils restreints, vous échangez souvent la taille du code / la taille de la mémoire / les performances d'une manière qu'il serait difficile de demander à un compilateur de faire. Par exemple, je sais que cette action utilisateur n'est pas appelée souvent, donc j'aurai une petite taille de code et de mauvaises performances, mais cette autre fonction qui semble similaire est utilisée chaque seconde, donc j'aurai une taille de code plus grande et des performances plus rapides. C'est le genre de compromis qu'un programmeur d'assemblage qualifié peut utiliser.
Je voudrais également ajouter qu'il y a beaucoup de terrain d'entente où vous pouvez coder en C compiler et examiner l'assembly produit, puis changer votre code C ou modifier et maintenir en tant qu'assembly.
Mon ami travaille sur des microcontrôleurs, actuellement des puces pour contrôler de petits moteurs électriques. Il travaille dans une combinaison de bas niveau c et d'assemblage. Il m'a dit une fois une bonne journée de travail où il a réduit la boucle principale de 48 instructions à 43. Il est également confronté à des choix tels que le code a grandi pour remplir la puce de 256k et que l'entreprise veut une nouvelle fonctionnalité, pensez-vous
Je voudrais ajouter en tant que développeur commercial avec un portefeuille ou des langages, des plates-formes, des types d'applications que je n'ai jamais ressenti le besoin de plonger dans l'écriture d'assemblage. J'ai toujours apprécié les connaissances que j'ai acquises à ce sujet. Et parfois débogué dedans.
Je sais que j'ai beaucoup plus répondu à la question "pourquoi devrais-je apprendre l'assembleur" mais je pense que c'est une question plus importante que quand est-ce plus rapide.
alors essayons encore une fois Vous devriez penser à l'assemblage
N'oubliez pas de comparer votre assemblage au compilateur généré pour voir lequel est plus rapide / plus petit / meilleur.
David.
la source
sbi
etcbi
) que les compilateurs (et parfois encore) ne profitent pas pleinement, en raison de leur connaissance limitée du matériel.Je suis surpris que personne n'ait dit cela. La
strlen()
fonction est beaucoup plus rapide si elle est écrite en assembleur! En C, la meilleure chose que vous puissiez faire estpendant l'assemblage, vous pouvez l'accélérer considérablement:
la longueur est en ecx. Cela compare 4 caractères à la fois, donc c'est 4 fois plus rapide. Et pensez qu'en utilisant le mot de poids fort eax et ebx, cela deviendra 8 fois plus rapide que la routine C précédente!
la source
(word & 0xFEFEFEFF) & (~word + 0x80808080)
est zéro si tous les octets dans word ne sont pas nuls.Les opérations matricielles utilisant des instructions SIMD sont probablement plus rapides que le code généré par le compilateur.
la source
Je ne peux pas donner d'exemples spécifiques car c'était il y a trop d'années, mais il y avait de nombreux cas où l'assembleur écrit à la main pouvait surpasser n'importe quel compilateur. Raisons pour lesquelles:
Vous pouvez dévier des conventions d'appel, en passant des arguments dans les registres.
Vous pouvez soigneusement réfléchir à l'utilisation des registres et éviter de stocker des variables en mémoire.
Pour des choses comme les tables de saut, vous pourriez éviter d'avoir à vérifier les limites de l'index.
Fondamentalement, les compilateurs font un très bon travail d'optimisation, et c'est presque toujours "assez bien", mais dans certaines situations (comme le rendu graphique) où vous payez cher pour chaque cycle, vous pouvez prendre des raccourcis parce que vous connaissez le code , où un compilateur ne pouvait pas parce qu'il devait être du bon côté.
En fait, j'ai entendu parler d'un code de rendu graphique où une routine, comme une routine de tracé de ligne ou de remplissage de polygone, générait en fait un petit bloc de code machine sur la pile et l'exécutait là, afin d'éviter une prise de décision continue sur le style de ligne, la largeur, le motif, etc.
Cela dit, ce que je veux qu'un compilateur fasse, c'est de générer un bon code d'assemblage pour moi mais pas trop intelligent, et ils le font surtout. En fait, une des choses que je déteste à propos de Fortran est de brouiller le code dans une tentative de «l'optimiser», généralement sans but significatif.
Habituellement, lorsque les applications ont des problèmes de performances, cela est dû à une conception inutile. Ces jours-ci, je ne recommanderais jamais l'assembleur pour les performances à moins que l'application globale n'ait déjà été réglée dans un pouce de sa vie, ne soit toujours pas assez rapide et passe tout son temps dans des boucles internes étroites.
Ajouté: J'ai vu de nombreuses applications écrites en langage assembleur, et le principal avantage de vitesse par rapport à un langage comme C, Pascal, Fortran, etc. était parce que le programmeur était beaucoup plus prudent lors du codage en assembleur. Il ou elle va écrire environ 100 lignes de code par jour, quelle que soit la langue, et dans un langage de compilation qui équivaut à 3 ou 400 instructions.
la source
Quelques exemples de mon expérience:
Accès à des instructions qui ne sont pas accessibles à partir de C.Par exemple, de nombreuses architectures (comme x86-64, IA-64, DEC Alpha et MIPS ou PowerPC 64 bits) prennent en charge une multiplication 64 bits par 64 bits produisant un résultat de 128 bits. GCC a récemment ajouté une extension donnant accès à ces instructions, mais avant que cet assemblage ne soit requis. Et l'accès à cette instruction peut faire une énorme différence sur les processeurs 64 bits lors de l'implémentation de quelque chose comme RSA - parfois autant qu'un facteur d'amélioration des performances.
Accès aux drapeaux spécifiques au CPU. Celui qui m'a beaucoup mordu est le drapeau de portage; lorsque vous effectuez un ajout à précision multiple, si vous n'avez pas accès au bit de report du processeur, vous devez plutôt comparer le résultat pour voir s'il a débordé, ce qui nécessite 3 à 5 instructions supplémentaires par membre; et pire encore, qui sont assez sériels en termes d'accès aux données, ce qui tue les performances sur les processeurs superscalaires modernes. Lors du traitement de milliers de tels entiers d'affilée, pouvoir utiliser addc est une énorme victoire (il y a aussi des problèmes superscalaires avec des conflits sur le bit de transport, mais les processeurs modernes s'en sortent plutôt bien).
SIMD. Même les compilateurs d'autovectorisation ne peuvent faire que des cas relativement simples, donc si vous voulez de bonnes performances SIMD, il est malheureusement souvent nécessaire d'écrire directement le code. Bien sûr, vous pouvez utiliser l'intrinsèque au lieu de l'assembly mais une fois que vous êtes au niveau intrinsèque, vous écrivez essentiellement l'assembly de toute façon, en utilisant simplement le compilateur comme un allocateur de registre et (nominalement) un planificateur d'instructions. (J'ai tendance à utiliser les intrinsèques pour SIMD simplement parce que le compilateur peut générer les prologues de la fonction et ainsi de suite pour moi, donc je peux utiliser le même code sous Linux, OS X et Windows sans avoir à traiter les problèmes ABI comme les conventions d'appel de fonction, mais d'autres que les intrinsèques SSE ne sont vraiment pas très belles - celles d'Altivec semblent meilleures même si je n'ai pas beaucoup d'expérience avec elles).Correction d'erreurs AES ou SIMD en bitslicing - on pourrait imaginer un compilateur capable d'analyser des algorithmes et de générer un tel code, mais il me semble qu'un tel compilateur intelligent est au moins à 30 ans de l'existant (au mieux).
D'un autre côté, les machines multicœurs et les systèmes distribués ont changé la plupart des gains de performances les plus importants dans l'autre sens - obtenez une accélération supplémentaire de 20% en écrivant vos boucles internes en assemblage, ou 300% en les exécutant sur plusieurs cœurs, ou 10000% par les exécuter sur un cluster de machines. Et bien sûr, les optimisations de haut niveau (des choses comme les futurs, la mémorisation, etc.) sont souvent beaucoup plus faciles à faire dans un langage de niveau supérieur comme ML ou Scala que C ou asm, et peuvent souvent fournir une performance beaucoup plus importante. Donc, comme toujours, il y a des compromis à faire.
la source
Boucles serrées, comme lors de la lecture d'images, car une image peut contenir des millions de pixels. S'asseoir et déterminer comment utiliser au mieux le nombre limité de registres de processeur peut faire une différence. Voici un exemple réel:
http://danbystrom.se/2008/12/22/optimizing-away-ii/
Ensuite, les processeurs ont souvent des instructions ésotériques trop spécialisées pour que le compilateur s'en soucie, mais parfois un programmeur assembleur peut en faire bon usage. Prenez par exemple l'instruction XLAT. Vraiment génial si vous devez faire des recherches de table en boucle et que la table est limitée à 256 octets!
Mise à jour: Oh, venez à penser à ce qui est le plus crucial lorsque nous parlons de boucles en général: le compilateur n'a souvent aucune idée du nombre d'itérations qui seront le cas commun! Seul le programmeur sait qu'une boucle sera répétée BEAUCOUP de fois et qu'il sera donc avantageux de la préparer avec un peu de travail supplémentaire, ou si elle sera répétée si peu de fois que la configuration prendra réellement plus de temps que les itérations attendu.
la source
Plus souvent que vous ne le pensez, C doit faire des choses qui semblent inutiles du point de vue d'un codeur d'assemblage simplement parce que les normes C le disent.
La promotion entière, par exemple. Si vous voulez décaler une variable char en C, on s'attendrait généralement à ce que le code fasse en fait juste cela, un décalage d'un seul bit.
Cependant, les normes imposent au compilateur d'étendre un signe à int avant le décalage et de tronquer le résultat en char par la suite, ce qui pourrait compliquer le code en fonction de l'architecture du processeur cible.
la source
Vous ne savez pas vraiment si votre code C bien écrit est vraiment rapide si vous n'avez pas examiné le désassemblage de ce que le compilateur produit. Plusieurs fois, vous le regardez et voyez que "bien écrit" était subjectif.
Il n'est donc pas nécessaire d'écrire dans l'assembleur pour obtenir le code le plus rapide de tous les temps, mais cela vaut certainement la peine de connaître l'assembleur pour la même raison.
la source
J'ai lu toutes les réponses (plus de 30) et je n'ai pas trouvé de raison simple: l'assembleur est plus rapide que C si vous avez lu et pratiqué le Manuel de référence de l'optimisation des architectures Intel® 64 et IA-32 , donc la raison pour laquelle l'assemblage peut être plus lent, c'est que les personnes qui écrivent un assemblage plus lent n'ont pas lu le manuel d'optimisation .
Dans le bon vieux temps d'Intel 80286, chaque instruction était exécutée avec un nombre fixe de cycles CPU, mais depuis Pentium Pro, sorti en 1995, les processeurs Intel sont devenus superscalaires, en utilisant Complex Pipelining: exécution hors commande et renommage de registre. Avant cela, sur Pentium, produit en 1993, il y avait des pipelines U et V: des pipelines doubles qui pouvaient exécuter deux instructions simples à un cycle d'horloge si elles ne dépendaient pas l'une de l'autre; mais ce n'était rien à comparer de ce qui est Exécution Hors Ordre & Renommage de Registre apparu dans Pentium Pro, et presque inchangé de nos jours.
Pour expliquer en quelques mots, le code le plus rapide est celui où les instructions ne dépendent pas des résultats précédents, par exemple, vous devez toujours effacer les registres entiers (par movzx) ou utiliser
add rax, 1
place ouinc rax
pour supprimer la dépendance sur l'état précédent des indicateurs, etc.Vous pouvez en savoir plus sur l'exécution hors-commande et le changement de nom du registre si le temps le permet, de nombreuses informations sont disponibles sur Internet.
Il y a aussi d'autres problèmes importants comme la prédiction de branche, le nombre d'unités de chargement et de stockage, le nombre de portes qui exécutent des micro-opérations, etc., mais la chose la plus importante à considérer est à savoir l'exécution hors service.
La plupart des gens ne sont tout simplement pas au courant de l'exécution hors service, ils écrivent donc leurs programmes d'assemblage comme pour 80286, s'attendant à ce que leur instruction prenne un temps fixe pour s'exécuter, quel que soit le contexte; tandis que les compilateurs C sont conscients de l'exécution hors service et génèrent correctement le code. C'est pourquoi le code de ces personnes inconscientes est plus lent, mais si vous en prenez conscience, votre code sera plus rapide.
la source
Je pense que le cas général où l'assembleur est plus rapide est quand un programmeur d'assemblage intelligent regarde la sortie du compilateur et dit "c'est un chemin critique pour les performances et je peux l'écrire pour être plus efficace", puis cette personne ajuste cet assembleur ou le réécrit de zéro.
la source
Tout dépend de votre charge de travail.
Pour les opérations quotidiennes, C et C ++ sont très bien, mais il y a certaines charges de travail (toutes les transformations impliquant la vidéo (compression, décompression, effets d'image, etc.)) qui nécessitent à peu près l'assemblage pour être performants.
Ils impliquent également généralement l'utilisation d'extensions de chipset spécifiques au processeur (MME / MMX / SSE / peu importe) qui sont réglées pour ce type d'opération.
la source
J'ai une opération de transposition de bits qui doit être faite, sur 192 ou 256 bits à chaque interruption, qui se produit toutes les 50 microsecondes.
Cela se fait par une carte fixe (contraintes matérielles). En utilisant C, il a fallu environ 10 microsecondes pour faire. Lorsque j'ai traduit cela en assembleur, en tenant compte des caractéristiques spécifiques de cette carte, de la mise en cache de registre spécifique et de l'utilisation d'opérations orientées bits; il a fallu moins de 3,5 microsecondes pour effectuer.
la source
Il pourrait être utile de regarder Optimizing Immutable and Purity de Walter Bright, ce n'est pas un test profilé, mais vous montre un bon exemple de différence entre l'ASM manuscrit et généré par le compilateur. Walter Bright écrit des compilateurs d'optimisation, donc il pourrait être utile de consulter ses autres articles de blog.
la source
LInux assembly howto , pose cette question et donne les avantages et les inconvénients de l'utilisation de assembly.
la source
La réponse simple ... Celui qui connaît bien l' assemblage (aka a la référence à côté de lui, et profite de chaque petite fonctionnalité de cache de processeur et de pipeline, etc.) est garanti d'être capable de produire du code beaucoup plus rapide que tout autre compilateur.
Cependant, la différence de nos jours n'a pas d'importance dans l'application typique.
la source
L'une des possibilités de la version CP / M-86 de PolyPascal (frère de Turbo Pascal) était de remplacer l'installation "use-bios-to-output-characters-to-the-screen" par une routine de langage machine qui, en substance, a reçu le x, et y, et la chaîne à mettre là.
Cela a permis de mettre à jour l'écran beaucoup, beaucoup plus vite qu'avant!
Il y avait de la place dans le binaire pour incorporer du code machine (quelques centaines d'octets) et il y avait aussi d'autres trucs, il était donc essentiel de presser autant que possible.
Il s'avère que puisque l'écran était de 80x25, les deux coordonnées pouvaient tenir dans un octet chacune, donc les deux pouvaient tenir dans un mot de deux octets. Cela a permis de faire les calculs nécessaires en moins d'octets puisqu'un seul ajout pouvait manipuler les deux valeurs simultanément.
À ma connaissance, il n'y a pas de compilateurs C qui peuvent fusionner plusieurs valeurs dans un registre, faire des instructions SIMD dessus et les diviser plus tard (et je ne pense pas que les instructions machine seront de toute façon plus courtes).
la source
L'un des extraits d'assemblage les plus célèbres provient de la boucle de mappage de texture de Michael Abrash ( expliquée en détail ici ):
De nos jours, la plupart des compilateurs expriment des instructions spécifiques au processeur avancées comme intrinsèques, c'est-à-dire des fonctions qui sont compilées jusqu'à l'instruction réelle. MS Visual C ++ prend en charge les éléments intrinsèques pour MMX, SSE, SSE2, SSE3 et SSE4, vous devez donc vous soucier moins de passer à l'assemblage pour tirer parti des instructions spécifiques à la plate-forme. Visual C ++ peut également tirer parti de l'architecture réelle que vous ciblez avec le paramètre / ARCH approprié.
la source
Avec le bon programmeur, les programmes Assembler peuvent toujours être réalisés plus rapidement que leurs homologues C (au moins marginalement). Il serait difficile de créer un programme C où vous ne pourriez pas retirer au moins une instruction de l'assembleur.
la source
http://cr.yp.to/qhasm.html contient de nombreux exemples.
la source
gcc est devenu un compilateur largement utilisé. Ses optimisations en général ne sont pas si bonnes. Beaucoup mieux que l'assembleur d'écriture programmeur moyen, mais pour de vraies performances, pas si bon. Il existe des compilateurs qui sont tout simplement incroyables dans le code qu'ils produisent. Donc, comme réponse générale, il y aura de nombreux endroits où vous pouvez aller dans la sortie du compilateur et modifier l'assembleur pour les performances, et / ou simplement réécrire la routine à partir de zéro.
la source
Longpoke, il n'y a qu'une seule limitation: le temps. Lorsque vous n'avez pas les ressources nécessaires pour optimiser chaque changement de code et passer votre temps à allouer des registres, à optimiser quelques déversements et quoi de plus, le compilateur gagnera à chaque fois. Vous faites votre modification du code, recompilez et mesurez. Répétez si nécessaire.
En outre, vous pouvez faire beaucoup de choses sur le côté de haut niveau. En outre, inspecter l'assemblage résultant peut donner l'impression que le code est de la merde, mais en pratique, il s'exécutera plus rapidement que ce que vous pensez serait plus rapide. Exemple:
int y = données [i]; // fais quelques trucs ici .. call_function (y, ...);
Le compilateur lira les données, les poussera à empiler (déversement) et lira plus tard à partir de la pile et passera comme argument. Ça a l'air merdique? Il peut en fait s'agir d'une compensation de latence très efficace et d'une exécution plus rapide.
// version optimisée call_function (data [i], ...); // pas si optimisé après tout ..
L'idée avec la version optimisée était que nous avons réduit la pression du registre et évité les déversements. Mais en vérité, la version "merdique" était plus rapide!
En regardant le code d'assemblage, en regardant simplement les instructions et en concluant: plus d'instructions, plus lentement, serait une erreur de jugement.
La chose ici à faire attention est la suivante: de nombreux experts en montage pensent qu’ils en savent beaucoup, mais en savent très peu. Les règles changent également de l'architecture à la suivante. Il n'y a pas de code Silver-bullet x86, par exemple, qui est toujours le plus rapide. Ces jours-ci, il vaut mieux suivre les règles de base:
En outre, faire trop confiance au compilateur pour transformer comme par magie un code C / C ++ mal pensé en code "théoriquement optimal" est un vœu pieux. Vous devez connaître le compilateur et la chaîne d'outils que vous utilisez si vous vous souciez des "performances" à ce bas niveau.
Les compilateurs en C / C ++ ne sont généralement pas très bons pour réorganiser les sous-expressions car les fonctions ont des effets secondaires, pour commencer. Les langages fonctionnels ne souffrent pas de cette mise en garde mais ne correspondent pas bien à l'écosystème actuel. Il existe des options de compilation pour permettre des règles de précision assouplies qui permettent de modifier l'ordre des opérations par le générateur de compilateur / éditeur de liens / code.
Ce sujet est un peu une impasse; pour la plupart, ce n'est pas pertinent, et le reste, ils savent déjà ce qu'ils font de toute façon.
Tout se résume à ceci: "comprendre ce que vous faites", c'est un peu différent de savoir ce que vous faites.
la source