web-dev-qa-db-fra.com

Pourquoi le compilateur ne peut-il pas (ou ne pas) optimiser une boucle d'addition prévisible dans une multiplication?

C'est une question qui m'est venue à l'esprit en lisant la réponse brillante de Mysticial à la question: pourquoi est-il plus rapide de traiter un tableau trié qu'un tableau non trié ?

Contexte pour les types impliqué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 est donc échange des boucles , déplace la branche en dehors 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 quiconque) puisse donner une réponse tout aussi brillante. Je n'ai jamais entendu parler des optimisations abordées dans cette autre question, alors je suis vraiment reconnaissant pour cela.

112
jhabbott

Le compilateur ne peut généralement pas transformer

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

dans

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

car ce dernier risque d’entraîner un débordement d’entiers signés alors que le premier ne le permet pas. Même avec un comportement enveloppant garanti en cas de dépassement des entiers du complément à deux signés, le résultat serait modifié (si data[c] est égal à 30000, le produit devient -1294967296 pour le code 32 bits typique ints avec une boucle enveloppante, tout en ajoutant 30000 à sum, si cela ne débordait pas, augmentait sum de 3000000000). Notez que la même chose vaut pour les quantités non signées, avec des nombres différents, un débordement de 100000 * data[c] introduirait généralement un modulo de réduction 2^32 qui ne doit pas apparaître dans le résultat final.

Cela pourrait le transformer en

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000LL * data[c];  // resp. 100000ull

cependant, si, comme d'habitude, long long est suffisamment plus grand que int.

Pourquoi ne fait-il pas cela, je ne peux pas dire, je suppose que c'est ce que dit Mysticial , "apparemment, il ne exécute pas une passe de réduction 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

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

peut conduire à déborder où

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];

ne serait pas. C'est casher ici, puisque la condition garantit que tous les data[c] qui sont ajoutés ont le même signe. Par conséquent, si l'un d'entre eux déborde, les deux le sont.

Je ne serais cependant pas sûr que le compilateur en tienne compte (@Mysticial, pouvez-vous essayer avec une condition telle que data[c] & 0x80 ou une règle qui peut être vraie pour les valeurs positives et négatives?). J'ai demandé aux compilateurs de faire des optimisations invalides (par exemple, il y a quelques années, j'avais un ICC (11.0, iirc) qui utilisait une conversion 32 bits signé en double en double dans 1.0/nn était un unsigned int. Était environ deux fois plus rapide que la sortie de gcc. Mais faux, beaucoup de valeurs étaient plus grandes que 2^31, oups.).

92
Daniel Fischer

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, l'addition répétée en virgule flottante n'est pas équivalente à la multiplication. Considérer:

float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;

float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;

float result2 = init;
result2 += step * count;

cout << (result1 - result2);

Démo: http://ideone.com/7RhfP

44
Ben Voigt

Le compilateur contient diverses passes qui font l’optimisation. Habituellement, dans chaque passe, une optimisation des instructions ou une optimisation de boucle sont effectuées. À l'heure actuelle, aucun modèle n'optimise le corps de la boucle en fonction des en-têtes de boucle. C'est difficile à détecter et moins commun. 

L'optimisation qui a été faite était un mouvement de code invariant de la boucle. Cela peut être fait en utilisant un ensemble de techniques.

5
knightrider

Eh bien, je suppose que certains compilateurs pourraient effectuer ce type d’optimisation, en supposant que nous parlions d’arithmétique entière.

En 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 intégraux unsigned, cela ne devrait pas faire de différence, car leur comportement de débordement est entièrement spécifié par le langage. Mais pour les signés, cela pourrait (probablement pas sur la plate-forme de complément à 2 cependant). 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 correct d'ignorer cette sémantique de débordement, mais tous les compilateurs ne sont pas assez courageux pour le faire. Cela fait souvent l'objet de nombreuses critiques de la part de la foule "C n'est qu'un langage de l'Assemblée de plus haut niveau". (Vous vous souvenez de ce qui s'est passé lorsque GCC a introduit des optimisations basées sur la sémantique de l'aliasing strict?)

Historiquement, GCC s’est révélé être un compilateur capable de prendre des mesures aussi radicales, mais d’autres compilateurs pourraient préférer s’en tenir au comportement «prévu par l’utilisateur» même s’il n’est pas défini par le langage.

3
AnT

Il y a une barrière conceptuelle à ce type d'optimisation. Les auteurs de compilateurs consacrent beaucoup d'efforts à réduction de la force - par exemple, remplacer les multiplications par des ajouts et des décalages. Ils s'habituent à penser que les multiplications sont mauvaises. Ainsi, un cas dans lequel il faut aller dans le sens contraire est surprenant et contre-intuitif. Donc, personne ne pense à le mettre en œuvre.

3
zwol

Les personnes qui développent et gèrent les compilateurs ont peu de temps et d'énergie à consacrer à leur travail. Elles souhaitent donc généralement se concentrer sur ce qui importe le plus à leurs utilisateurs: transformer du code bien écrit en code rapide. Ils ne veulent pas passer leur temps à essayer de trouver des moyens de transformer un code stupide en code rapide - c'est à cela que sert l'examen de code. Dans un langage de haut niveau, il peut exister un code "stupide" qui exprime une idée importante, ce qui vaut la peine que les développeurs ont le temps de le faire rapidement - par exemple, la déforestation raccourcie et la fusion de flux permettent aux programmes Haskell structurés autour de certains types de paresseux structures de données produites pour être compilées en boucles serrées qui n'allouent pas de mémoire. Mais ce type d'incitation ne s'applique tout simplement pas à transformer une addition en boucle en une multiplication. Si vous voulez que ce soit rapide, écrivez-le simplement avec la multiplication.

0
dfeuer