C'est une question qui m'est venue à l'esprit en lisant la brillante réponse de Mysticial à la question: pourquoi est-il plus rapide de traiter un tableau trié qu'un tableau non trié ?
Contexte des types concernés:
const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;
Dans sa réponse, il explique que le compilateur Intel (ICC) optimise ceci:
for (int i = 0; i < 100000; ++i)
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += data[c];
... en quelque chose d'équivalent à ceci:
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
for (int i = 0; i < 100000; ++i)
sum += data[c];
L'optimiseur reconnaît que celles-ci sont équivalentes et échange donc les boucles , déplaçant la branche hors de la boucle interne. Très intelligent!
Mais pourquoi ne fait-il pas cela?
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += 100000 * data[c];
Espérons que Mysticial (ou n'importe qui d'autre) puisse donner une réponse tout aussi brillante. Je n'ai jamais entendu parler des optimisations évoquées dans cette autre question auparavant, donc j'en suis vraiment reconnaissant.
c
performance
compiler-optimization
Jhabbott
la source
la source
volatile
, alors l'échange de boucle serait également une optimisation invalide.Réponses:
Le compilateur ne peut généralement pas transformer
dans
car ce dernier pourrait entraîner un débordement d'entiers signés alors que le premier ne le fait pas. Même avec un comportement de bouclage garanti pour le dépassement des entiers du complément à deux signés, cela changerait le résultat (si
data[c]
c'est 30000, le produit deviendrait-1294967296
pour les 32 bits typiquesint
avec un bouclage, tandis que 100000 fois en ajoutant 30000 ausum
serait, si cela ne déborde pas, augmentesum
de 3000000000). Notez que la même chose vaut pour les quantités non signées, avec des nombres différents, un dépassement de100000 * data[c]
capacité introduirait généralement un modulo de réduction2^32
qui ne doit pas apparaître dans le résultat final.Cela pourrait le transformer en
cependant, si, comme d'habitude,
long long
est suffisamment plus grand queint
.Pourquoi ça ne fait pas ça, je ne peux pas dire, je suppose que c'est ce que Mysticial a dit , "apparemment, il n'exécute pas de passe de boucle après échange de boucle".
Notez que l'échange de boucle lui-même n'est généralement pas valide (pour les entiers signés), car
peut conduire à un débordement où
pas. C'est casher ici, car la condition garantit
data[c]
que tout ce qui est ajouté a le même signe, donc si l'un déborde, les deux le font.Je ne serais pas trop sûr que le compilateur ait pris cela en compte (@Mysticial, pourriez-vous essayer avec une condition comme
data[c] & 0x80
ou alors qui peut être vraie pour les valeurs positives et négatives?). J'ai demandé à des compilateurs de faire des optimisations non valides (par exemple, il y a quelques années, j'avais une conversion ICC (11.0, iirc) avec une conversion signée-32-bit-int-to-double dans1.0/n
oùn
était ununsigned int
. Était environ deux fois plus rapide que gcc Mais faux, beaucoup de valeurs étaient plus grandes que2^31
, oups.).la source
ADD.W A6,$A000
, en oubliant que les opérations de mot avec des registres d'adresses signent le mot à 32 bits avant l'ajout. Il a fallu un certain temps pour dépanner, car la seule chose que le code a faite entre celaADD
et la prochaine fois qu'il a sorti A6 de la pile était de restaurer les registres de l'appelant qu'il a enregistrés dans cette trame ...MyArray[0] = 4;
je pouvais vérifier l'adresse deMyArray
, et regarder cet emplacement avant et après l'instruction exécutée; ça ne changerait pas. Le code était quelque chose commemove.B @A3,#4
et A3 était censé toujours pointer versMyArray
chaque fois que l'instruction s'exécutait, mais ce n'était pas le cas. Amusement.Cette réponse ne s'applique pas au cas spécifique lié, mais elle s'applique au titre de la question et peut intéresser les futurs lecteurs:
En raison de la précision finie, une addition à virgule flottante répétée n'est pas équivalente à une multiplication . Considérer:
Démo
la source
Le compilateur contient plusieurs passes qui effectuent l'optimisation. Habituellement, à chaque passe, une optimisation des instructions ou des optimisations de boucle sont effectuées. À l'heure actuelle, il n'y a pas de modèle qui effectue une optimisation du corps de la boucle basée sur les en-têtes de boucle. Ceci est difficile à détecter et moins courant.
L'optimisation qui a été effectuée était un mouvement de code invariant en boucle. Cela peut être fait en utilisant un ensemble de techniques.
la source
Eh bien, je suppose que certains compilateurs pourraient faire ce type d'optimisation, en supposant que nous parlons d'arithmétique entière.
Dans le même temps, certains compilateurs peuvent refuser de le faire car le remplacement de l'addition répétitive par la multiplication peut modifier le comportement de débordement du code. Pour les types entiers non signés, cela ne devrait pas faire de différence car leur comportement de débordement est entièrement spécifié par le langage. Mais pour ceux signés, cela pourrait (probablement pas sur la plate-forme complémentaire de 2). Il est vrai que le débordement signé conduit en fait à un comportement indéfini en C, ce qui signifie qu'il devrait être parfaitement OK d'ignorer complètement cette sémantique de débordement, mais tous les compilateurs ne sont pas assez courageux pour le faire. Il suscite souvent beaucoup de critiques de la part du "C est juste un langage d'assemblage de plus haut niveau". (Vous vous souvenez de ce qui s'est passé lorsque GCC a introduit des optimisations basées sur une sémantique d'aliasing strict?)
Historiquement, GCC s'est montré comme un compilateur qui a ce qu'il faut pour prendre des mesures aussi drastiques, mais d'autres compilateurs pourraient préférer s'en tenir au comportement perçu «destiné à l'utilisateur» même s'il n'est pas défini par le langage.
la source
C'est le cas maintenant - du moins, clang fait :
compile avec -O1 pour
Le débordement d'entier n'a rien à voir avec cela; s'il y a un débordement d'entier qui provoque un comportement indéfini, cela peut se produire dans les deux cas. Voici le même type de fonction utilisant
int
au lieu delong
:compile avec -O1 pour
la source
Il y a une barrière conceptuelle à ce type d'optimisation. Les auteurs de compilateurs consacrent beaucoup d'efforts à la réduction de la force - par exemple, en remplaçant les multiplications par des ajouts et des décalages. Ils s'habituent à penser que les multiplications sont mauvaises. Donc, un cas où l'on devrait aller dans l'autre sens est surprenant et contre-intuitif. Personne ne pense donc à le mettre en œuvre.
la source
Les personnes qui développent et maintiennent des compilateurs ont un temps et une énergie limités à consacrer à leur travail, ils veulent donc généralement se concentrer sur ce qui intéresse le plus leurs utilisateurs: transformer un code bien écrit en code rapide. Ils ne veulent pas passer leur temps à essayer de trouver des moyens de transformer un code idiot en code rapide - c'est à cela que sert la révision du code. Dans un langage de haut niveau, il peut y avoir du code «idiot» qui exprime une idée importante, ce qui vaut le temps des développeurs pour faire cela rapidement - par exemple, la déforestation raccourcie et la fusion de flux permettent aux programmes Haskell structurés autour de certains types de paresseux produit des structures de données à compiler en boucles serrées qui n'allouent pas de mémoire. Mais ce genre d'incitation ne s'applique tout simplement pas à transformer l'addition en boucle en multiplication. Si vous voulez que ce soit rapide,
la source