web-dev-qa-db-fra.com

Y a-t-il quelque chose de spécial à propos de -1 (0xFFFFFFFF) concernant l'ADC?

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!)

38
Cassio Neri

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.

https://agner.org/optimize/

33
Peter Cordes