Comme évoqué dans d'autres réponses/commentaires, l'utilisation de memcmp(a,b,4) < 0
est équivalent à un unsigned
comparaison entre des entiers big-endian. Il ne pouvait pas être mis en ligne aussi efficacement que == 0
sur x86 little-endian.
Plus important encore, la version actuelle de ce comportement dans gcc7/8 ne cherche que memcmp() == 0
o != 0
. Même sur une cible big-endian où cela pourrait être mis en ligne tout aussi efficacement pour <
o >
gcc ne le fera pas. (Les compilateurs big-endian les plus récents de Godbolt sont PowerPC 64 gcc6.3, et MIPS/MIPS64 gcc5.4. mips
est un MIPS big-endian, tandis que mipsel
est little-endian MIPS). Si vous testez ceci avec un futur gcc, utilisez a = __builtin_assume_align(a, 4)
pour s'assurer que gcc n'a pas à s'inquiéter des performances et de la correction des charges non alignées sur les systèmes non-x86. (Ou simplement utiliser const int32_t*
au lieu de const char*
.)
Si/quand gcc apprend à mettre en ligne memcmp
pour les cas autres que EQ/NE, peut-être que gcc le fera sur x86 little-endian quand son heuristique lui dira que la taille du code supplémentaire en vaut la peine. par exemple, dans une boucle chaude lors de la compilation avec -fprofile-use
(optimisation guidée par le profil).
Si vous voulez que les compilateurs fassent un bon travail pour ce cas de figure vous devriez probablement l'attribuer à un uint32_t
et utiliser une fonction de conversion endienne comme ntohl
. Mais assurez-vous d'en choisir un qui puisse réellement être en ligne ; apparemment Windows dispose d'un ntohl
qui se compile en un appel DLL . Voir les autres réponses à cette question pour des informations sur le portable-endian, et aussi la tentative imparfaite de quelqu'un de portable_endian.h
et ceci la fourche de celui-ci . J'ai travaillé sur une version pendant un certain temps, mais je ne l'ai jamais terminée/testée ni postée.
Le transfert de pointeur peut être un comportement non défini, selon la façon dont vous avez écrit les octets et ce que les char*
Les points suivants . Si vous n'êtes pas sûr de l'alignement et/ou de l'anticrénelage strict, memcpy
en abytes
. La plupart des compilateurs sont bons pour optimiser l'élimination des petits objets de taille fixe. memcpy
.
// I know the question just wonders why gcc does what it does,
// not asking for how to write it differently.
// Beware of alignment performance or even fault issues outside of x86.
#include <endian.h>
#include <stdint.h>
int equal4_optim(const char* a, const char* b) {
uint32_t abytes = *(const uint32_t*)a;
uint32_t bbytes = *(const uint32_t*)b;
return abytes == bbytes;
}
int less4_optim(const char* a, const char* b) {
uint32_t a_native = be32toh(*(const uint32_t*)a);
uint32_t b_native = be32toh(*(const uint32_t*)b);
return a_native < b_native;
}
J'ai vérifié sur Godbolt et qui compile un code efficace (fondamentalement identique à ce que j'ai écrit en asm ci-dessous), surtout sur les plateformes big-endian, même avec un vieux gcc. Il fait également un bien meilleur code que ICC17, qui inlines memcmp
mais seulement à une boucle de comparaison d'octets (même pour le format == 0
cas.
Je pense que cette séquence faite à la main est une mise en œuvre optimale de less4()
(pour la convention d'appel SystemV x86-64, comme celle utilisée dans la question, avec const char *a
en rdi
y b
en rsi
).
less4:
mov edi, [rdi]
mov esi, [rsi]
bswap edi
bswap esi
# data loaded and byte-swapped to native unsigned integers
xor eax,eax # solves the same problem as gcc's movzx, see below
cmp edi, esi
setb al # eax=1 if *a was Below(unsigned) *b, else 0
ret
Il s'agit de toutes les instructions single-uop sur les CPU Intel et AMD depuis K8 et Core2 ( http://agner.org/optimize/ ).
Le fait de devoir bswap les deux opérandes a un coût supplémentaire en termes de taille de code par rapport à l'option == 0
cas : nous ne pouvons pas plier l'un des chargements en une opérande de la mémoire pour cmp
. (Cela permet d'économiser de la taille de code, et des uops grâce à la micro-fusion). bswap
des instructions.
Sur les processeurs qui supportent movbe
il permet d'économiser la taille du code : movbe ecx, [rsi]
est une charge + bswap. Sur Haswell, c'est 2 uops, donc on peut supposer que ça décode les mêmes uops que mov ecx, [rsi]
/ bswap ecx
. Sur Atom/Silvermont, c'est géré directement dans les ports de chargement, donc moins d'uops ainsi qu'une taille de code plus petite.
Voir le site setcc
une partie de ma réponse de xor-zeroing pour en savoir plus sur la raison pour laquelle xor/cmp/setcc (que clang utilise) est meilleur que cmp/setcc/movzx (typique pour gcc).
Dans le cas habituel où cela s'insère dans du code qui se branche sur le résultat, l'option setcc + zero-extension sont remplacés par un jcc ; le compilateur optimise la création d'une valeur de retour booléenne dans un registre. C'est encore un autre avantage de l'inlining : la bibliothèque memcmp
doit créer une valeur de retour booléenne entière que l'appelant teste. car aucune convention d'appel ou ABI x86 ne permet de renvoyer des conditions booléennes dans les drapeaux. (Je ne connais pas non plus de convention d'appel non-x86 qui le permette). Pour la plupart des bibliothèques memcmp
les implémentations, il y a aussi une surcharge significative dans le choix d'une stratégie en fonction de la longueur, et peut-être la vérification de l'alignement. Cela peut être assez bon marché, mais pour la taille 4, cela va être plus que le coût de tout le travail réel.