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
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:
// 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 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:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
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 une ffs()
fonction, 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 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é vers bswap
(é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.