Dans un de mes projets de recherche, j'écris du code C++. Cependant, l'assemblage généré est l'un des points cruciaux du projet. C++ ne fournit pas un accès direct aux instructions de manipulation d'indicateurs, en particulier à ADC
mais cela ne devrait pas être un problème à condition que le compilateur soit suffisamment intelligent pour l'utiliser. Considérer:
constexpr unsigned X = 0;
unsigned f1(unsigned a, unsigned b) {
b += a;
unsigned c = b < a;
return c + b + X;
}
La variable c
est une solution de contournement pour mettre la main sur le drapeau de report et l'ajouter à b
et X
. Il semble que j'ai eu de la chance et le (g++ -O3
, version 9.1) le code généré est le suivant:
f1(unsigned int, unsigned int):
add %edi,%esi
mov %esi,%eax
adc $0x0,%eax
retq
Pour toutes les valeurs de X
que j'ai testées, le code est comme ci-dessus (sauf, bien sûr, pour la valeur immédiate $0x0
qui change en conséquence). J'ai trouvé une exception cependant: quand X == -1
(ou 0xFFFFFFFFu
ou ~0u
, ... peu importe comment vous l'orthographiez) le code généré est:
f1(unsigned int, unsigned int):
xor %eax,%eax
add %edi,%esi
setb %al
lea -0x1(%rsi,%rax,1),%eax
retq
Cela semble moins efficace que le code initial comme le suggèrent les mesures indirectes (pas très scientifique cependant) Ai-je raison? Si oui, est-ce un type de bogue "opportunité d'optimisation manquante" qui est mérite d'être signalé?
Pour ce qui vaut, clang -O3
, version 8.8.0, utilise toujours ADC
(comme je le voulais) et icc -O3
, la version 19.0.1 ne le fait jamais.
J'ai essayé d'utiliser l'intrinsèque _addcarry_u32
mais cela n'a pas aidé.
unsigned f2(unsigned a, unsigned b) {
b += a;
unsigned char c = b < a;
_addcarry_u32(c, b, X, &b);
return b;
}
Je pense que je n'utilise peut-être pas _addcarry_u32
correctement (je n'ai pas trouvé beaucoup d'informations dessus). Quel est l'intérêt de l'utiliser car c'est à moi de fournir le drapeau de portage? (Encore une fois, en introduisant c
et en priant pour que le compilateur comprenne la situation.)
Je pourrais, en fait, l'utiliser correctement. Pour X == 0
Je suis heureux:
f2(unsigned int, unsigned int):
add %esi,%edi
mov %edi,%eax
adc $0x0,%eax
retq
Pour X == -1
Je suis malheureuse :-(
f2(unsigned int, unsigned int):
add %esi,%edi
mov $0xffffffff,%eax
setb %dl
add $0xff,%dl
adc %edi,%eax
retq
J'obtiens le ADC
mais ce n'est clairement pas le code le plus efficace. (Que fait dl
là-bas? Deux instructions pour lire le drapeau de transport et le restaurer? Vraiment? J'espère que je me trompe!)
mov
+ adc $-1, %eax
est plus efficace que xor
- zéro + setc
+ 3 composants lea
pour la latence et le nombre d'uop sur la plupart des CPU, et pas pire sur les CPU toujours pertinents .1
Cela ressemble à une optimisation manquée par gcc : il voit probablement un cas spécial et se verrouille dessus, se tirant dans le pied et empêchant le adc
la reconnaissance des formes se produit.
Je ne sais pas exactement ce qu'il a vu/recherchait, alors oui, vous devez signaler cela comme un bug d'optimisation manquée. Ou si vous voulez creuser plus profondément vous-même, vous pouvez regarder la sortie GIMPLE ou RTL après les passes d'optimisation et voir ce qui se passe. Si vous savez quelque chose sur les représentations internes de GCC. Godbolt a une fenêtre de vidage d'arbre GIMPLE que vous pouvez ajouter à partir du même menu déroulant que "compilateur de clones".
Le fait que clang le compile avec adc
prouve qu'il est légal, c'est-à-dire que l'asm que vous voulez correspond à la source C++, et vous n'avez pas manqué un cas spécial qui empêche le compilateur de faire cette optimisation. (En supposant que clang est exempt de bogues, ce qui est le cas ici.)
Ce problème peut certainement se produire si vous ne faites pas attention, par exemple essayer d'écrire une fonction adc
de cas général qui prend en charge et fournit l'exécution à partir de l'addition à 3 entrées est difficile en C, car l'un ou l'autre des deux ajouts peut être transporté, vous ne pouvez donc pas simplement utiliser le sum < a+b
idiome après avoir ajouté le report à l'une des entrées. Je ne suis pas sûr qu'il soit possible d'obtenir gcc ou clang pour émettre add/adc/adc
où le milieu adc
doit reprendre et produire le report.
par exemple. 0xff...ff + 1
passe à 0, donc sum = a+b+carry_in
/carry_out = sum < a
ne peut pas être optimisé en adc
car il doit ignorer porter dans le cas spécial où a = -1
et carry_in = 1
.
Donc une autre supposition est que peut-être gcc a envisagé de faire le + X
plus tôt, et s'est tiré une balle dans le pied à cause de ce cas particulier. Cela n'a cependant pas beaucoup de sens.
Quel est l'intérêt de l'utiliser car c'est à moi de fournir le drapeau de portage?
Vous utilisez _addcarry_u32
correctement.
Le but de son existence est de vous permettre d'exprimer un add avec carry in ainsi que carry out, ce qui est difficile en C. CCC pur et clang don 'optimise pas bien, souvent pas seulement en gardant le résultat de portage dans CF.
Si vous ne souhaitez que la réalisation, vous pouvez fournir un 0
comme report et il sera optimisé en add
au lieu de adc
, mais vous donnera toujours le report en tant que variable C.
par exemple. pour ajouter deux entiers 128 bits en segments 32 bits, vous pouvez le faire
// bad on x86-64 because it doesn't optimize the same as 2x _addcary_u64
// even though __restrict guarantees non-overlap.
void adc_128bit(unsigned *__restrict dst, const unsigned *__restrict src)
{
unsigned char carry;
carry = _addcarry_u32(0, dst[0], src[0], &dst[0]);
carry = _addcarry_u32(carry, dst[1], src[1], &dst[1]);
carry = _addcarry_u32(carry, dst[2], src[2], &dst[2]);
carry = _addcarry_u32(carry, dst[3], src[3], &dst[3]);
}
(Sur Godbolt avec GCC/clang/ICC)
C'est très inefficace contre unsigned __int128
où les compilateurs utilisent simplement add/adc 64 bits, mais obtiennent clang et ICC pour émettre une chaîne de add
/adc
/adc
/adc
. GCC fait un gâchis, en utilisant setcc
pour stocker CF dans un entier pour certaines des étapes, puis add dl, -1
pour le remettre dans CF pour un adc
.
GCC aspire malheureusement à la précision étendue/biginteger écrit en C. Clang pur fait parfois un peu mieux, mais la plupart des compilateurs sont mauvais. C'est pourquoi les fonctions gmplib de niveau le plus bas sont écrites à la main en asm pour la plupart des architectures.
Note de bas de page 1 : ou pour le nombre d'uop: égal sur Intel Haswell et antérieur où adc
est 2 uops, sauf avec un zéro immédiat où Sandybridge -cas spécial décodeurs de la famille que comme 1 uop.
Mais le LEA à 3 composants avec un base + index + disp
en fait une instruction de latence à 3 cycles sur les processeurs Intel, c'est donc bien pire.
Sur Intel Broadwell et versions ultérieures, adc
est une instruction à 1 uop même avec un immédiat non nul, profitant de la prise en charge des uops à 3 entrées introduites avec Haswell pour FMA.
Le nombre total d'uop est donc égal mais la latence pire signifie que adc
serait toujours un meilleur choix.