L’une des raisons invoquées pour connaître assembleur est qu’il peut parfois être utilisé pour écrire un code plus performant que l’écriture dans un langage de niveau supérieur, le langage C en particulier. Cependant, j’ai aussi entendu dire à maintes reprises que, bien que ce ne soit pas tout à fait faux, les cas dans lesquels l’assembleur peut réellement être utilisé pour générer du code plus performant sont: à la fois extrêmement rare et nécessitent une connaissance approfondie et une expérience de l’assemblage.
Cette question n'entre même pas dans le fait que les instructions d'assembleur seront spécifiques à une machine et non portables, ni à aucun des autres aspects de l'assembleur. Bien sûr, il existe de nombreuses bonnes raisons de connaître Assembly à part celle-ci, mais il s’agit là d’une question spécifique sollicitant des exemples et des données, et non d’un discours étendu sur les langages assembleurs et les langages supérieurs.
Quelqu'un peut-il fournir des exemples spécifiques de cas où Assembly serait plus rapide qu'un code C bien écrit utilisant un compilateur moderne, et pouvez-vous appuyer cette affirmation avec le profilage preuve? 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 point de discorde.
Voici un exemple concret: le point fixe se multiplie sur les anciens compilateurs.
Celles-ci sont non seulement utiles sur les appareils sans virgule flottante, elles brillent également en termes de précision car elles 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 uniforme absolue sur toute la plage, au lieu d'une précision relative relative (float
).
Les compilateurs modernes optimisent bien cet exemple en virgule fixe. Pour des exemples plus modernes qui nécessitent encore du code spécifique au compilateur, voir
uint64_t
_ pour les multiplications 32x32 => 64 bits ne parvient pas à être optimisée sur un processeur 64 bits, vous avez donc besoin de fonctions intrinsèques ou ___int128
_ pour un code efficace sur les systèmes 64 bits.C n’a pas d’opérateur de multiplication complète (résultat de 2N bits des entrées de N bits). La manière habituelle de l'exprimer en C consiste à convertir les entrées en un type plus large en espérant que le compilateur reconnaît que les bits supérieurs des entrées ne sont pas intéressants:
_// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
long long a_long = a; // cast to 64 bit.
long long product = a_long * b; // perform multiplication
return (int) (product >> 16); // shift by the fixed point bias
}
_
Le problème avec ce code est que nous faisons quelque chose qui ne peut pas être exprimé directement dans le langage C. Nous voulons multiplier deux nombres 32 bits et obtenir un résultat de 64 bits dont nous renvoyons 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 les multiplier par 64 * 64 = 64.
x86 (et ARM, MIPS et autres) peuvent toutefois faire la multiplication en une seule instruction. Certains compilateurs avaient l'habitude d'ignorer ce fait et de générer 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).
Nous nous retrouvons donc avec un ou deux appels de bibliothèque, juste pour une multiplication. Cela a de graves conséquences. Non seulement le décalage est-il plus lent, mais les registres doivent être préservés tout au long des appels de fonctions;.
Si vous réécrivez le même code dans l'assembleur (en ligne), vous pouvez augmenter considérablement la vitesse.
En plus de cela: utiliser 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 en tant que __emul et le décalage 64 bits en tant que __ll_rshift.
En utilisant des éléments 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 d’insérer le code en ligne, d’allouer aux registres, d’éliminer les sous-expressions communes et de propager de manière constante. Vous obtiendrez ainsi une énorme amélioration des performances par rapport au code assembleur écrit à la main.
Pour référence: Le résultat final du mul à virgule fixe du compilateur VS.NET est:
_int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
_
La différence de performance des divisions en points fixes est encore plus grande. J'ai eu des améliorations jusqu'à facteur 10 pour la division du code à points fixes lourds en écrivant quelques lignes asm.
L'utilisation de Visual C++ 2013 donne le même code d'assembly dans les deux sens.
gcc4.1 à partir de 2007 optimise également la version C pure. (L’explorateur du compilateur Godbolt n’a pas de version antérieure de gcc installée, mais il est probable que même les versions antérieures de GCC pourraient le faire sans éléments intrinsèques.)
Voir source + asm pour x86 (32 bits) et ARM on l'explorateur du compilateur Godbolt . (Malheureusement, aucun compilateur n’a été assez vieux pour produire du code incorrect à partir de la version C pure et simple.)
Les processeurs modernes peuvent faire des choses C n'a pas d'opérateur pour du tout , comme popcnt
ou bit-scan pour trouver le premier ou dernier bit défini . (POSIX a une fonction ffs()
, mais sa sémantique ne correspond pas à x86 bsf
/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 la compiler en une instruction popcnt
(si elle est activée à la compilation), mais il est beaucoup plus fiable d’utiliser ___builtin_popcnt
_ dans GNUC, ou sur x86 si vous ne ciblez que du matériel avec SSE4.2: _mm_popcnt_u32
_ À PARTIR DE _<immintrin.h>
.
Ou en C++, assignez à un _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 compilera toujours quelque chose de correct et pourra 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 compiler pour bswap
(permutation d'octets x86 32 bits pour la conversion endian) sur certaines implémentations en C qui en sont équipées.
La vectorisation manuelle à l’aide des instructions SIMD est un autre domaine important pour les asm intrinsèques ou manuscrits. 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 automatiquement quand les choses se compliquent. 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 de code scalaire.
Il y a de nombreuses années, j'apprenais à 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 a été réduit à environ 30 secondes avec le compilateur non optimisant dont il disposait.
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'assemblage généré par le compilateur et, d'après ce que j'ai vu, j'ai alors décidé que mon temps d'écriture en assembleur était terminé.
Quasiment chaque fois que le compilateur voit du code en virgule flottante, une version manuscrite sera plus rapide. La raison principale est que le compilateur ne peut effectuer aucune optimisation robuste. Voir cet article à partir de MSDN pour une discussion sur le sujet. Voici un exemple où la version Assembly est deux fois plus rapide que la version C (compilée avec VS2K5):
#include "stdafx.h"
#include <windows.h>
float KahanSum
(
const float *data,
int n
)
{
float
sum = 0.0f,
C = 0.0f,
Y,
T;
for (int i = 0 ; i < n ; ++i)
{
Y = *data++ - C;
T = sum + Y;
C = T - sum - Y;
sum = T;
}
return sum;
}
float AsmSum
(
const float *data,
int n
)
{
float
result = 0.0f;
_asm
{
mov esi,data
mov ecx,n
fldz
fldz
l1:
fsubr [esi]
add esi,4
fld st(0)
fadd st(0),st(2)
fld st(0)
fsub st(0),st(3)
fsub st(0),st(2)
fstp st(2)
fstp st(2)
loop l1
fstp result
fstp result
}
return result;
}
int main (int, char **)
{
int
count = 1000000;
float
*source = new float [count];
for (int i = 0 ; i < count ; ++i)
{
source [i] = static_cast <float> (Rand ()) / static_cast <float> (Rand_MAX);
}
LARGE_INTEGER
start,
mid,
end;
float
sum1 = 0.0f,
sum2 = 0.0f;
QueryPerformanceCounter (&start);
sum1 = KahanSum (source, count);
QueryPerformanceCounter (&mid);
sum2 = AsmSum (source, count);
QueryPerformanceCounter (&end);
cout << " C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;
return 0;
}
Et certains numéros de mon PC exécutant une version par défaut*:
C code: 500137 in 103884668
asm code: 500137 in 52129147
Par intérêt, j'ai échangé la boucle avec un déc/jnz et cela n'a eu aucune incidence sur les minutages - parfois plus rapide, parfois plus lent. Je suppose que l’aspect limité en mémoire prime sur d’autres optimisations.
Oups, je courais une version légèrement différente du code et il a sorti les nombres dans le mauvais sens (c.-à-d. Que C était plus rapide!). Correction et mise à jour des résultats.
Sans donner d'exemples spécifiques ni de preuves de profileur, vous pouvez écrire un meilleur assembleur que le compilateur si 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 de processeurs, il peut essayer de réorganiser les instructions plus rapidement que ne le peut un humain, et ainsi de suite. un ordinateur étant aussi bon ou meilleur que le meilleur joueur humain pour les jeux de société, etc., tout simplement parce qu'il peut effectuer des recherches plus rapidement dans l'espace du problème 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 infaisable dans plusieurs cas (c'est-à-dire que le compilateur vous surpassera certainement si vous essayez d'écrire plus que quelques routines en assembleur).
D'autre part, il existe 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érique, où l'assembleur, combiné à la connaissance intime du matériel en question, peut donner de meilleurs résultats que ceux d'un compilateur C.
D'autres ont mentionné des instructions spéciales, ce dont je parle dans le paragraphe ci-dessus, instructions dont le compilateur aurait peut-être une connaissance limitée ou aucune connaissance, permettant à un humain d'écrire du code plus rapidement.
Dans mon travail, il y a trois raisons pour que je connaisse et utilise Assembly. Par ordre d'importance:
Débogage - Je reçois souvent un code de bibliothèque contenant des bogues ou une documentation incomplète. Je découvre ce qu’il fait en intervenant au niveau de l’Assemblée. Je dois le faire environ une fois par semaine. Je l'utilise également comme un outil pour déboguer des problèmes dans lesquels mes yeux ne repèrent pas l'erreur idiomatique dans C/C++/C #. En regardant l'Assemblée, ça passe.
Optimisation - le compilateur réussit assez bien à l'optimisation, mais je joue dans un stade différent de la plupart des autres. J'écris un code de traitement d'image qui commence généralement par un code qui ressemble à ceci:
for (int y=0; y < imageHeight; y++) {
for (int x=0; x < imageWidth; x++) {
// do something
}
}
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 des cycles dans cette phase "faire quelque chose", les gains de performance sont énormément amplifiés. Je ne commence généralement pas par là - je commence généralement par écrire le code pour commencer, puis je fais de mon mieux pour refactoriser le C afin qu'il soit naturellement meilleur (meilleur algorithme, moins de charge dans la boucle, etc.). J'ai généralement besoin de lire Assembly pour voir ce qui se passe et rarement besoin de l'écrire. Je le fais peut-être tous les deux ou trois mois.
faire quelque chose que la langue ne me laissera pas faire. Ceux-ci incluent - obtenir l'architecture du processeur et ses fonctionnalités spécifiques, accéder aux indicateurs qui ne sont pas dans la CPU (man, je souhaite vraiment que C vous donne accès à l'indicateur de port), etc. Je le fais peut-être une fois par an ou tous les deux ans.
Ce n'est que lorsque vous utilisez des ensembles d'instructions spécifiques que le compilateur ne prend pas en charge.
Pour maximiser la puissance de calcul d'un processeur moderne avec plusieurs pipelines et branchements prédictifs, vous devez structurer le programme Assembly de manière à rendre a) presque impossible la rédaction par un humain b), voire 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 supérieur à la performance par rapport aux micro-optimisations que vous pouvez réaliser dans Assembly.
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 en virgule fixe: Le produit de deux nombres de 16 bits est un nombre de 32 bits. Mais les règles en C stipulent que le produit de deux nombres de 16 bits est un nombre de 16 bits et que le produit de deux nombres de 32 bits est un nombre de 32 bits - la moitié inférieure dans les deux cas. Si vous voulez la moitié top d'une multiplication 16x16 ou d'une multiplication 32x32, vous devez jouer à des jeux avec le compilateur. La méthode générale consiste à attribuer une largeur de bit supérieure à celle nécessaire, à la multiplier, à la décaler et à la rejeter:
int16_t x, y;
// int16_t is a typedef for "short"
// set x and y to something
int16_t prod = (int16_t)(((int32_t)x*y)>>16);`
Dans ce cas, le compilateur peut être assez intelligent pour savoir que vous essayez simplement de multiplier la moitié supérieure d'un 16x16 et de faire ce qu'il faut avec le multiplicateur 16x16 natif de la machine. Ou bien cela peut être stupide et nécessiter un appel de bibliothèque pour multiplier par 32 x32, ce qui est excessif car vous n'avez besoin que de 16 bits du produit - mais le standard C ne vous donne aucun moyen de vous exprimer.
Certaines opérations de bitshifting (rotation/carry):
// 256-bit array shifted right in its entirety:
uint8_t x[32];
for (int i = 32; --i > 0; )
{
x[i] = (x[i] >> 1) | (x[i-1] << 7);
}
x[0] >>= 1;
Ce n'est pas trop inélégant en C, mais encore une fois, à moins que le compilateur soit assez intelligent pour comprendre ce que vous faites, il va faire beaucoup de travail "inutile". De nombreux jeux d'instructions d'assemblage vous permettent d'effectuer une rotation ou un décalage gauche/droite avec le résultat dans le registre de report. Vous pouvez donc effectuer les opérations ci-dessus en 34 instructions: chargez un pointeur au début du tableau, effacez le report et effectuez l'exécution. bit décale à droite, en utilisant l'incrémentation automatique sur le pointeur.
Pour un autre exemple, il existe registres à décalage à retour linéaire (LFSR) qui sont élégamment réalisés dans Assembly: prenez un bloc de N bits (8, 16, 32, 64, 128, etc.), déplacez le tout chose à droite de 1 (voir algorithme ci-dessus), si le report 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 de contraintes sérieuses en termes de performances. Comme d'autres l'ont déjà dit, l'assemblage est beaucoup plus difficile à documenter/déboguer/tester/maintenir que le code C: le gain de performances entraîne des coûts importants.
edit: 3. La détection de débordement est possible dans Assembly (on ne peut pas vraiment le faire en C), cela rend certains algorithmes beaucoup plus faciles.
Réponse courte? Quelquefois.
Techniquement, chaque abstraction a un coût et un langage de programmation est une abstraction du fonctionnement du processeur. C est cependant très proche. Il y a des années, je me souvenais de rire aux éclats lorsque je me suis connecté à mon compte UNIX et que j'ai reçu le message de fortune suivant (quand de telles choses étaient populaires):
Le langage de programmation C - Un langage qui combine la flexibilité du langage d'assemblage avec la puissance du langage d'assemblage.
C'est drôle parce que c'est vrai: C est comme un langage d'assemblage portable.
Il est à noter que le langage d'assemblage ne fonctionne que lorsque vous l'écrivez. Il existe cependant un compilateur entre le langage C et le langage d'assemblage qu'il génère, ce qui est extrêmement important car à quelle vitesse votre code C a-t-il beaucoup à voir avec la qualité de votre compilateur?
Lorsque gcc est arrivé sur les lieux, l’un des éléments qui l’a rendu si populaire est qu’il était souvent bien meilleur que les compilateurs C livrés avec de nombreuses versions UNIX commerciales. Non seulement c'était ANSI C (aucune de ces ordures K & R C), il était plus robuste et produisait généralement un code meilleur (plus rapide). Pas toujours mais souvent.
Je vous dis tout cela parce qu'il n'y a pas de règle générale concernant la vitesse de C et de l'assembleur car il n'y a pas de standard objectif pour C.
De même, l'assembleur varie beaucoup selon le processeur utilisé, les spécifications de votre système, le jeu d'instructions que vous utilisez, etc. Historiquement, il existait deux familles d'architectures de CPU: CISC et RISC. Le principal acteur de l'ICCA était et reste l'architecture Intel x86 (et le jeu d'instructions). RISC a dominé le monde UNIX (MIPS6000, Alpha, Sparc, etc.). L'ICCA a remporté la bataille des cœurs et des esprits.
Quoi qu'il en soit, lorsque j'étais un développeur plus jeune, la sagesse populaire était que x86 manuscrit pouvait souvent être beaucoup plus rapide que C, car l'architecture fonctionnait de manière complexe et bénéficiait d'une complexité tirée par un humain. RISC, d’autre part, semblait conçu pour les compilateurs, c’est pourquoi personne (je le savais) n’écrit ce que dit assembleur Sparc. Je suis sûr que de telles personnes existaient, mais il ne fait aucun doute qu'elles sont devenues folles et institutionnalisées à présent.
Les jeux d'instructions constituent un point important, même dans la même famille de processeurs. Certains processeurs Intel ont des extensions telles que SSE à SSE4. AMD avait ses propres instructions SIMD. L'avantage d'un langage de programmation comme le C était que quelqu'un pouvait écrire sa bibliothèque, donc il était optimisé pour le processeur sur lequel vous exécutiez le logiciel. C'était un travail difficile dans l'assembleur.
Il existe toujours des optimisations que vous pouvez effectuer dans l'assembleur qu'aucun compilateur ne pourrait en faire et un algorithme d'assembleur bien écrit sera aussi rapide ou plus rapide que son équivalent en C. La plus grande question est: est-ce que cela en vaut la peine?
En fin de compte, l’assembleur était un produit de son époque et était plus populaire à une époque où les cycles de processeur étaient coûteux. De nos jours, un processeur dont la fabrication coûte 5 à 10 dollars (Intel Atom) peut faire à peu près tout ce que n'importe qui peut souhaiter. La seule vraie raison d'écrire 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), des pilotes de périphérique, éventuellement des périphériques intégrés (même si le C a tendance à y dominer aussi) et ainsi de suite. Ou juste pour les coups de pied (ce qui est un peu masochiste).
Point un qui n'est pas la réponse.
Même si vous n'y programmez jamais, je trouve utile de connaître au moins un jeu d'instructions assembleur. Cela fait partie de la quête sans fin des programmeurs pour en savoir plus et donc être mieux. Également utile lorsque vous entrez dans des frameworks pour lesquels vous n'avez pas le code source et qui 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 les puces intégrées, où la faible complexité des puces et la faible concurrence dans les compilateurs ciblant ces puces peuvent faire pencher la balance en faveur des humains. De plus, pour les périphériques restreints, vous négociez souvent la taille du code/la taille de la mémoire/les performances d'une manière difficile à charger d'un compilateur. par exemple. Je sais que cette action utilisateur n'est pas appelée souvent. Par conséquent, la taille du code est petite et les performances médiocres, mais cette autre fonction similaire est utilisée toutes les secondes. Par conséquent, la taille du code est plus grande et les performances plus rapides. C'est le genre de compromis qu'un programmeur d'assemblage qualifié peut utiliser.
Je voudrais aussi ajouter qu’il existe de nombreux moyens pour coder en C compiler et examiner l’Assemblée produite, puis changer le code C ou modifier et maintenir en Assemblage.
Mon ami travaille sur des micro-contrôleurs, actuellement des puces pour contrôler de petits moteurs électriques. Il travaille dans une combinaison de bas niveau c et d'assemblage. Une fois, il m'a parlé d'une bonne journée de travail au cours de laquelle il a réduit la boucle principale de 48 instructions à 43. Il est également confronté à des choix tels que le code a évolué pour remplir la puce 256k et que l'entreprise souhaite une nouvelle fonctionnalité.
J'aimerais 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'Assembly. J'ai toujours apprécié les connaissances que j'en ai acquises. Et parfois y déboguer.
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 le moment où c'est plus rapide.
alors essayons encore une fois Vous devriez penser à l'Assemblée
N'oubliez pas de comparer votre assemblage au compilateur généré pour voir lequel est le plus rapide/le plus petit/le mieux.
David.
Un cas d’utilisation qui pourrait ne plus s’appliquer mais pour votre plus grand plaisir: sur Amiga, le processeur et les puces graphiques/audio se disputeraient pour accéder à une certaine zone de RAM (les 2 premiers Mo de RAM être spécifique). Ainsi, lorsque vous ne disposez que de 2 Mo RAM (ou moins), l'affichage de graphiques complexes ainsi que la lecture du son tue les performances du processeur.
En assembleur, vous pouvez entrelacer votre code de manière si astucieuse que le processeur ne tenterait d’accéder à la RAM que lorsque les puces graphiques/audio étaient occupées en interne (c’est-à-dire lorsque le bus était libre). Ainsi, en réordonnant vos instructions, en utilisant intelligemment le cache du processeur, la synchronisation du bus, vous pouvez obtenir des effets qui ne sont tout simplement pas possibles avec un langage de niveau supérieur, car vous devez chronométrer chaque commande, voire insérer des NOP ici et là pour conserver les différents paramètres. jetons hors de chaque radar.
C’est une autre raison pour laquelle l’instruction NOP (pas d’opération - ne rien faire) du processeur peut en réalité accélérer l’ensemble de votre application.
[EDIT] Bien sûr, la technique dépend d'une configuration matérielle spécifique. Quelle était la raison principale pour laquelle de nombreux jeux Amiga ne pouvaient pas gérer des processeurs plus rapides: le timing des instructions était mauvais.
Les opérations matricielles utilisant des instructions SIMD sont probablement plus rapides que le code généré par le compilateur.
Je suis surpris que personne n'ait dit ça. La fonction strlen()
est beaucoup plus rapide si elle est écrite dans Assembly! En C, la meilleure chose à faire est
int c;
for(c = 0; str[c] != '\0'; c++) {}
alors que dans Assembly, vous pouvez accélérer considérablement:
mov esi, offset string
mov edi, esi
xor ecx, ecx
lp:
mov ax, byte ptr [esi]
cmp al, cl
je end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp
end_4:
inc esi
end_3:
inc esi
end_2:
inc esi
end_1:
inc esi
mov ecx, esi
sub ecx, edi
la longueur est en ecx. Cela compare 4 caractères à la fois, donc 4 fois plus rapide. Et pensez en utilisant le mot d'ordre élevé de eax et ebx, il deviendra 8 fois plus rapide que la précédente routine C!
Je ne peux pas vous donner d'exemples précis, car c'était il y a trop longtemps, mais il y avait beaucoup de cas où l'assembleur écrit à la main pouvait surpasser n'importe quel compilateur. Raisons pour lesquelles:
Vous pouvez vous écarter des conventions d’appel en passant des arguments dans des registres.
Vous pouvez examiner avec soin la manière d'utiliser les registres et éviter de stocker des variables en mémoire.
Pour des choses comme les tables de sauts, vous pourriez éviter de vous limiter à vérifier l'index.
En gros, les compilateurs font un assez bon travail d'optimisation, ce qui est presque toujours "suffisant", mais dans certaines situations (comme le rendu graphique) où vous payez cher pour chaque cycle, vous pouvez utiliser des raccourcis, car vous connaissez le code. , où un compilateur ne pourrait pas, car il doit être du bon côté.
En fait, j'ai entendu parler d'un code de rendu graphique dans lequel une routine, telle qu'une routine de traçage de lignes ou de remplissage de polygones, générait en réalité un petit bloc de code machine sur la pile et l'exécutait à cet endroit, afin d'éviter une prise de décision continue. sur le style de trait, la largeur, le motif, etc.
Cela dit, ce que je veux qu'un compilateur fasse, c'est générer du bon code Assembly pour moi, sans être trop intelligent, et ils le font principalement. En fait, une des choses que je déteste chez Fortran, c’est de brouiller le code pour tenter de l’optimiser, généralement sans aucun but important.
Généralement, lorsque les applications rencontrent des problèmes de performances, cela est dû à une conception peu rentable. Ces jours-ci, je ne recommanderais jamais Assembler pour des performances excepté si l'application globale avait déjà été ajustée en un pouce de sa vie, n'était toujours pas assez rapide et passait tout son temps dans des boucles internes serrées.
Ajoutée: J'ai vu de nombreuses applications écrites en langage d'assemblage, et le principal avantage en termes de vitesse par rapport à un langage comme C, Pascal, Fortran, etc., était 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 utilisée, et dans un langage de compilateur égal à 3 ou 400 instructions.
Quelques exemples de mon expérience:
Accès à des instructions qui ne sont pas accessibles depuis C. Par exemple, de nombreuses architectures (telles que x86-64, IA-64, DEC Alpha et 64 bits MIPS ou PowerPC) prennent en charge une multiplication de 64 bits sur 64 bits produisant un résultat de 128 bits. GCC a récemment ajouté une extension donnant accès à de telles instructions, mais cette assemblée était requise auparavant. Et l’accès à cette instruction peut faire une énorme différence sur les processeurs 64 bits lors de l’implémentation de RSA - parfois jusqu’à 4 fois l’amélioration des performances.
Accès aux indicateurs spécifiques à la CPU. Celui qui m’a beaucoup mordu est le drapeau de retenue; lors de l'ajout de plusieurs précisions, si vous n'avez pas accès au bit de retenue de la CPU, vous devez plutôt comparer le résultat pour voir s'il déborde, ce qui prend 3 à 5 instructions supplémentaires par membre; et pire encore, qui sont assez série en termes d'accès aux données, ce qui tue les performances sur les processeurs superscalaires modernes. Le fait de pouvoir utiliser addc représente un avantage considérable lors du traitement de milliers de tels entiers à la suite (il existe également des problèmes superscalaires liés aux conflits sur le portage, mais les processeurs modernes le gèrent assez bien).
SIMD. Même les compilateurs autovectorisants ne peuvent traiter que des cas relativement simples. Par conséquent, si vous souhaitez obtenir de bonnes performances SIMD, il est malheureusement souvent nécessaire d'écrire le code directement. Bien sûr, vous pouvez utiliser des composants intrinsèques au lieu de Assembly, mais une fois que vous êtes au niveau intrinsèque, vous écrivez de manière générale Assembly, en utilisant simplement le compilateur comme un allocateur de registre et un planificateur d’instructions (nominalement). (J'ai tendance à utiliser les éléments intrinsèques pour SIMD simplement parce que le compilateur peut générer les prologues de la fonction et ainsi de suite pour que je puisse utiliser le même code sous Linux, OS X et Windows sans avoir à gérer des problèmes ABI tels que les conventions d'appel de fonction, mais d'autres. que les SSE intrinsèques ne sont vraiment pas très gentils - ceux d’Altivec semblent meilleurs, même si je n’ai pas beaucoup d’expérience avec eux). Comme exemples de choses qu'un compilateur vectorisant (jour actuel) ne peut pas comprendre, lisez à propos de découpage de bits AES ou correction d'erreur SIMD - on pourrait imaginer un compilateur capable d'analyser des algorithmes et générer un tel code, mais il me semble qu’un compilateur aussi intelligent est au moins dans 30 ans (au mieux).
D'autre part, les machines multicœurs et les systèmes distribués ont déplacé bon nombre des gains de performances les plus importants dans l'autre sens - accélération supplémentaire de 20% pour l'écriture de vos boucles internes dans Assembly, soit 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 (futures, mémoisation, etc.) sont souvent beaucoup plus faciles à réaliser dans un langage de niveau supérieur tel que ML ou Scala que C ou asm, et peuvent souvent générer des gains de performances bien plus importants. . Donc, comme toujours, il y a des compromis à faire.
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 processeurs peut faire la différence. Voici un échantillon de la vie réelle:
http://danbystrom.se/2008/12/22/optimizing-away-ii/
Ensuite, les processeurs ont souvent des instructions ésotériques trop spécialisées pour un compilateur, mais un programmeur assembleur peut parfois en faire bon usage. Prenez l'instruction XLAT par exemple. Vraiment génial si vous avez besoin de faire des recherches de table dans une boucle et la table est limitée à 256 octets!
Mise à jour: Oh, réfléchissez simplement à 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 sera le cas courant! Seul le programmeur sait qu'une boucle sera itérée BEAUCOUP de fois et qu'il sera donc bénéfique de la préparer avec un travail supplémentaire, ou si elle est itérée si peu de fois que la configuration prend plus de temps que les itérations. attendu.
Plus souvent que vous ne le pensez, C a besoin de faire des choses qui semblent inutiles du point de vue du codeur de l’Assemblée simplement parce que les normes C le disent bien.
Promotion entière, par exemple. Si vous voulez décaler une variable de caractère en C, on s’attend généralement à ce que le code le fasse en réalité, un décalage d’un bit à l’autre.
Les normes, cependant, obligent le compilateur à faire une extension de signe à int avant le décalage et à tronquer le résultat à char après, ce qui pourrait compliquer le code en fonction de l'architecture du processeur cible.
Vous ne savez pas réellement si votre code C bien écrit est vraiment rapide si vous n'avez pas examiné le démontage 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 assembleur pour obtenir le code le plus rapide à ce jour, mais il vaut certainement la peine de connaître assembleur pour la même raison.
Je pense que le cas général quand assembleur est plus rapide, c'est quand un programmeur d'assemblage intelligent regarde la sortie du compilateur et dit "c'est un chemin critique pour la performance et je peux écrire ceci pour être plus efficace" et ensuite cette personne peaufine cet assembleur ou la réécrit de zéro.
Tout dépend de votre charge de travail.
Pour les opérations quotidiennes, C et C++ conviennent parfaitement, mais il existe certaines charges de travail (toutes les transformations impliquant de la vidéo (compression, décompression, effets d'image, etc.)) qui nécessitent assez bien que Assembly soit performant.
Ils impliquent également généralement l’utilisation d’extensions de chipset spécifiques au processeur (MME/MMX/SSE/peu importe) adaptées à ce type d’opération.
J'ai une opération de transposition de bits qui doit être effectuée sur 192 ou 256 bits toutes les interruptions, ce qui se produit toutes les 50 microsecondes.
Cela se passe par une carte fixe (contraintes matérielles). En utilisant C, cela a pris environ 10 microsecondes. Lorsque j'ai traduit cela en Assembler, en prenant en compte les caractéristiques spécifiques de cette carte, la mise en cache de registres spécifique et l'utilisation d'opérations orientées bit; il a fallu moins de 3,5 microsecondes pour effectuer.
J'ai lu toutes les réponses (plus de 30) et je n'ai pas trouvé de raison simple: assembleur est plus rapide que C si vous avez lu et pratiqué les Manuel de référence de l'optimisation des architectures Intel® 64 et IA-32 , la raison pour laquelle l’Assemblée est peut-être plus lente est que les personnes qui écrivent un tel appareil plus lentement n’ont pas lu le Manuel d’optimisation..
Dans le bon vieux temps d'Intel 80286, chaque instruction était exécutée selon un nombre fixe de cycles de processeur, mais depuis Pentium Pro, sorti en 1995, les processeurs Intel sont devenus superscalaires grâce à l'utilisation de la technologie Complex Pipelining: Out-of-Order Execution & Register Rename. Avant cela, sur Pentium, produit en 1993, il existait des pipelines en U et en V: des canalisations doubles pouvant 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 entre ce qui est Exécution hors service et Renommer Registre sont apparus dans Pentium Pro et sont restés presque inchangés 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 devriez toujours effacer les registres entiers (avec movzx) ou utiliser add rax, 1
à la place ou inc rax
pour supprimer la dépendance sur l'état précédent des drapeaux, etc.
Vous pouvez en savoir plus sur Exécution hors-ligne et changement de nom de registre si le temps le permet, de nombreuses informations sont disponibles sur Internet.
Il existe également d'autres problèmes importants tels que la prédiction de branche, le nombre d'unités de charge et de stockage, le nombre de portes qui exécutent des micro-opérations, etc., mais le point le plus important à prendre en compte est notamment l'exécution hors séquence.
La plupart des gens ne sont tout simplement pas au courant de l’exécution non conforme aux instructions. Ils écrivent donc leurs programmes d’assemblage comme pour 80286; ils s’attendent à ce que leur instruction prenne un certain temps pour s’exécuter quel que soit le contexte; tandis que les compilateurs C sont au courant de l’exécution des commandes et génèrent le code correctement. C'est pourquoi le code de ces personnes ignorantes est plus lent, mais si vous en prenez conscience, votre code sera plus rapide.
Cela vaut peut-être la peine de regarder Optimizing Immutable and Purity de Walter Bright ce n'est pas un test profilé, mais vous montre un bon exemple de la différence entre un ASM écrit à la main et un compilateur. Walter Bright écrit des compilateurs optimisateurs, il serait donc intéressant de consulter ses autres articles.
LInux Assembly howto , pose cette question et donne les avantages et les inconvénients de l’utilisation de Assembly.
La réponse simple ... Celui qui sait Assembly bien (alias la référence à côté de lui et tire parti de chaque petite fonctionnalité de cache de processeur et de pipeline, etc.), est garanti être capable de produire du code beaucoup plus rapide que tout compilateur.
Cependant, la différence de nos jours n'a pas d'importance dans l'application typique.
gcc est devenu un compilateur largement utilisé. Ses optimisations en général ne sont pas si bonnes. Bien meilleur que le programmeur moyen qui écrit en assembleur, mais pour des performances réelles, ce n’est pas si bon. Il existe des compilateurs dont le code est tout simplement incroyable. En règle générale, il existe de nombreux endroits où vous pouvez accéder à la sortie du compilateur et ajuster l'assembleur pour obtenir des performances et/ou simplement réécrire la routine à partir de zéro.
Longpoke, il n'y a qu'une seule limitation: le temps. Lorsque vous ne disposez pas des ressources nécessaires pour optimiser chaque modification de code et que vous passez votre temps à attribuer des registres, à optimiser quelques débordements, le compilateur gagne à chaque fois. Vous modifiez le code, recompilez et mesurez. Répétez si nécessaire.
En outre, vous pouvez faire beaucoup de choses à haut niveau. En outre, l'inspection de l'assemblage obtenu peut donner à l'IMPRESSION le fait que le code est de la merde, mais en pratique, l'exécution sera plus rapide que ce que vous pensez être plus rapide. Exemple:
int y = données [i]; // fait des trucs ici .. call_function (y, ...);
Le compilateur lira les données, les poussera vers la pile (spill) et les lira plus tard à partir de la pile et les passera en argument. Ça sonne chier? La compensation de la latence peut s'avérer très efficace et permettre 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 avions réduit la pression des registres et évité les débordements. Mais en vérité, la version "merde" était plus rapide!
Regarder le code de l’assemblée, ne regarder que les instructions et conclure: plus d’instructions, moins vite, serait une erreur de jugement.
Ce qu'il faut retenir, c'est que de nombreux experts de l'Assemblée réfléchissent ils en savent beaucoup, mais très peu. Les règles changent également d'une architecture à l'autre. Par exemple, il n’existe pas de code silver-bullet x86, qui est toujours le plus rapide. Ces jours-ci est préférable d'aller par des règles empiriques:
Aussi, faire trop confiance au compilateur pour transformer par magie un code C/C++ mal pensé en un code "théoriquement optimal" constitue un voeu pieux. Vous devez connaître le compilateur et la chaîne d'outils que vous utilisez si vous vous souciez de la "performance" à ce niveau bas.
Les compilateurs en C/C++ ne sont généralement pas très doués 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 s'intègrent pas bien à l'écosystème actuel. Il existe des options de compilateur permettant des règles de précision assouplies, qui permettent au compilateur/éditeur de liens/générateur de code de modifier l'ordre des opérations.
Ce sujet est un peu une impasse; pour la plupart ce n'est pas pertinent, et le reste, ils savent ce qu'ils font déjà de toute façon.
Tout se résume à ceci: "pour comprendre ce que vous faites", c'est un peu différent de savoir ce que vous faites.
Que diriez-vous de création de code machine au moment de l'exécution?
Un jour, mon frère (environ 2000) a réalisé un traceur de rayons temps réel extrêmement rapide en générant du code au moment de l’exécution. Je ne me souviens pas des détails, mais il y avait une sorte de module principal qui parcourait les objets en boucle, puis il préparait et exécutait un code machine spécifique à chaque objet.
Cependant, avec le temps, cette méthode a été dépassée par le nouveau matériel graphique et est devenue inutile.
Aujourd'hui, je pense que cette méthode pourrait optimiser certaines opérations sur des données volumineuses (des millions d'enregistrements) telles que les tableaux croisés dynamiques, les forages, les calculs à la volée, etc. La question est: l'effort en vaut-il la peine?
L'un des extraits les plus célèbres d'Assembly provient de la boucle de mappage de texture de Michael Abrash ( expliqué en détail ici ):
add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps
De nos jours, la plupart des compilateurs expriment des instructions avancées spécifiques au processeur sous forme d’intrinsèques, c’est-à-dire des fonctions compilées jusqu’à l’instruction proprement dite. MS Visual C++ prend en charge les composants intrinsèques pour MMX, SSE, SSE2, SSE3 et SSE4. Vous n'avez donc pas à vous soucier de passer à Assembly 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é.
Avec le bon programmeur, les programmes Assembler peuvent toujours être créés plus rapidement que leurs homologues C (au moins marginalement). Il serait difficile de créer un programme C où vous ne pourriez pas supprimer au moins une instruction de l’Assembleur.
Une des possibilités de la version CP/M-86 de PolyPascal (apparentée à Turbo Pascal) était de remplacer l’installation "utilisation du bios vers les caractères de sortie vers l’écran" par une routine de langage machine qui a été donné le x, et y, et la chaîne pour y mettre.
Cela a permis de mettre à jour l'écran beaucoup, beaucoup plus rapidement qu'avant!
Il y avait de la place dans le binaire pour incorporer du code machine (quelques centaines d'octets) et il y avait d'autres trucs là aussi, il était donc essentiel de compresser autant que possible.
Il s'avère que puisque l'écran mesure 80 x 25, les deux coordonnées peuvent tenir dans un octet chacune, afin que les deux puissent tenir dans un mot de deux octets. Cela permettait de faire les calculs nécessaires en moins d’octets puisqu’un seul ajout pouvait manipuler les deux valeurs simultanément.
À ma connaissance, aucun compilateur C ne peut fusionner plusieurs valeurs dans un registre, faire des instructions SIMD dessus et les séparer à nouveau plus tard (et je ne pense pas que les instructions machine seront de toute façon plus courtes).
http://cr.yp.to/qhasm.html a de nombreux exemples.
La question est un peu trompeuse. La réponse est là dans votre message lui-même. Il est toujours possible d'écrire une solution d'assemblage pour un problème particulier qui s'exécute plus rapidement que celui généré par un compilateur. Le problème, c’est que vous devez être un expert en assemblage pour surmonter les limites d’un compilateur. Un programmeur d'assemblage expérimenté peut écrire dans n'importe quelle HLL des programmes plus rapides que ceux écrits par des inexpérimentés. La vérité est que vous pouvez toujours écrire des programmes Assembly s'exécutant plus rapidement que ceux générés par un compilateur.
Il est très difficile de répondre précisément à cette question, car la question n’est pas spécifique: qu’est-ce exactement un "compilateur moderne"?
Pratiquement toute optimisation d'assembleur manuelle pourrait théoriquement être réalisée par un compilateur. Qu'il soit réellement est done ne peut pas être dit en général, il ne s'agit que d'une version spécifique d'un compilateur spécifique. Beaucoup nécessitent probablement autant d'efforts pour déterminer si elles peuvent être appliquées sans effets secondaires dans un contexte particulier que les auteurs de compilateur ne s'en préoccupent pas.
Dans les jours où la vitesse du processeur était mesurée en MHz et la taille de l'écran inférieure à 1 mégapixel, une astuce bien connue pour obtenir un affichage plus rapide consistait à dérouler des boucles: opération d'écriture pour chaque ligne de balayage de l'écran. Il évitait les frais généraux liés au maintien d'un index de boucle! Couplé à la détection du rafraîchissement de l'écran, il s'est révélé très efficace.
C'est quelque chose qu'un compilateur C ne ferait pas ... (bien que vous puissiez souvent choisir entre l'optimisation pour la vitesse ou pour la taille, je suppose que le premier utilise des astuces similaires.)
Je sais que certaines personnes aiment écrire des applications Windows en langage Assembly. Ils prétendent qu'ils sont plus rapides (difficiles à prouver) et plus petits (en effet!).
Évidemment, même si c'est amusant à faire, c'est probablement du temps perdu (sauf pour des raisons d'apprentissage, bien sûr!), En particulier pour les opérations d'interface graphique ... Maintenant, peut-être quelques opérations, comme la recherche d'une chaîne dans un fichier , peut être optimisé par un code de montage écrit avec soin.
En fait, vous pouvez construire des programmes à grande échelle dans un grand modèle. Les segments peuvent être limités à un code de 64 Ko mais vous pouvez écrire de nombreux segments, les gens donnent l'argument contre ASM car il s'agit d'un ancien langage et nous n'avons plus besoin de préserver la mémoire. pourquoi aurions-nous bourré de mémoire sur notre PC, le seul défaut que je puisse trouver avec ASM est qu’il est plus ou moins basé sur un processeur, de sorte que la plupart des programmes écrits pour l’architecture Intel ne fonctionneraient probablement pas sur une architecture AMD. En ce qui concerne le fait que C est plus rapide que ASM, il n’existe pas de langage aussi rapide que ASM et ASM peut faire beaucoup de choses en C et d’autres HLL ne peuvent pas le faire au niveau du processeur. L'ASM est une langue difficile à apprendre, mais une fois que vous l'apprenez, aucun HLL ne peut le traduire mieux que vous. Si vous ne pouviez que voir certaines des choses que le HLL faisait pour vous et comprendre ce qu’il faisait, vous vous demanderiez pourquoi de plus en plus de gens n’utilisent pas ASM et pourquoi les assignations ne sont plus mises à jour (de toute façon pour le grand public). Donc, pas de C n'est pas plus rapide que ASM. Même les expériences des programmeurs C++ utilisent et écrivent toujours du code. Des morceaux dans ASM ont été ajoutés au code C++ pour plus de rapidité. Autres langues Aussi, certaines personnes pensent que ce sont des idées obsolètes ou que rien n’est bon est un mythe par exemple, par exemple, Photoshop est écrit en Pascal/ASM. TCL et ASM ... un dénominateur commun entre ceux-ci: "Les processeurs d’image rapides et performants sont ASM, bien que photoshop ait peut-être été mis à niveau en Delphi, c’est toujours Pascal. Tous les problèmes de vitesse viennent de Pascal, mais c Je voudrais créer un clone Photoshop en pur ASM sur lequel j’ai travaillé et qui fonctionne assez bien. Pas de code, d’interpréter, d’organiser, de réécrire, etc. et aller processus terminé.
Je dirais que lorsque vous êtes meilleur que le compilateur pour un ensemble d'instructions donné. Donc, pas de réponse générique je pense
De nos jours, en considérant des compilateurs comme Intel C++ qui optimisent extrêmement le code C, il est très difficile de concurrencer les sorties des compilateurs.