web-dev-qa-db-fra.com

Quand, si jamais, le déroulement de la boucle est toujours utile?

J'ai essayé d'optimiser du code extrêmement critique en termes de performances (un algorithme de tri rapide qui est appelé des millions et des millions de fois dans une simulation de Monte-Carlo) en déroulant la boucle. Voici la boucle intérieure que j'essaye d'accélérer:

// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}

J'ai essayé de dérouler quelque chose comme:

while(true) {
    if(myArray[++index1] < pivot) break;
    if(myArray[++index1] < pivot) break;
    // More unrolling
}


while(true) {
    if(pivot < myArray[--index2]) break;
    if(pivot < myArray[--index2]) break;
    // More unrolling
}

Cela n'a fait absolument aucune différence, je l'ai donc modifié pour le rendre plus lisible. J'ai eu des expériences similaires d'autres fois où j'ai essayé de dérouler la boucle. Compte tenu de la qualité des prédicteurs de branche sur du matériel moderne, quand, si jamais, le déroulement de la boucle est-il toujours une optimisation utile?

87
dsimcha

Le déroulement de la boucle est logique si vous pouvez briser les chaînes de dépendance. Cela donne à un CPU hors service ou super-scalaire la possibilité de mieux planifier les choses et donc de fonctionner plus rapidement.

Un exemple simple:

for (int i=0; i<n; i++)
{
  sum += data[i];
}

Ici, la chaîne de dépendance des arguments est très courte. Si vous obtenez un blocage parce que vous avez un cache-manque sur le tableau de données, le processeur ne peut rien faire d'autre que d'attendre.

Par contre ce code:

for (int i=0; i<n; i+=4)
{
  sum1 += data[i+0];
  sum2 += data[i+1];
  sum3 += data[i+2];
  sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

pourrait fonctionner plus rapidement. Si vous obtenez un échec de cache ou un autre blocage dans un calcul, il y a encore trois autres chaînes de dépendance qui ne dépendent pas du blocage. Un processeur hors service peut les exécuter.

112
Nils Pipenbrinck

Cela ne ferait aucune différence car vous faites le même nombre de comparaisons. Voici un meilleur exemple. Au lieu de:

for (int i=0; i<200; i++) {
  doStuff();
}

écrire:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

Même alors, cela n'aura certainement pas d'importance, mais vous faites maintenant 50 comparaisons au lieu de 200 (imaginez que la comparaison est plus complexe).

Manuel le déroulement de la boucle en général est cependant largement un artefact de l'histoire. C'est une autre liste croissante de choses qu'un bon compilateur fera pour vous quand cela compte. Par exemple, la plupart des gens ne prennent pas la peine d'écrire x <<= 1 ou x += x au lieu de x *= 2. Vous écrivez simplement x *= 2 et le compilateur l'optimisera pour vous pour ce qui est le mieux.

Fondamentalement, il est de moins en moins nécessaire de deviner votre compilateur.

22
cletus

Indépendamment de la prédiction de branche sur du matériel moderne, la plupart des compilateurs effectuent le déroulement de boucle pour vous de toute façon.

Il serait utile de savoir combien d'optimisations votre compilateur fait pour vous.

J'ai trouvé présentation de Felix von Leitner très instructif sur le sujet. Je vous recommande de le lire. Résumé: Les compilateurs modernes sont TRÈS intelligents, donc les optimisations de main ne sont presque jamais efficaces.

14
Peter Alexander

Le déroulement de la boucle, qu'il s'agisse du déroulement manuel ou du déroulement du compilateur, peut souvent être contre-productif, en particulier avec les processeurs x86 plus récents (Core 2, Core i7). Conclusion: testez votre code avec et sans déroulement de boucle sur les processeurs sur lesquels vous prévoyez de déployer ce code.

2
Paul R

Pour autant que je le comprenne, les compilateurs modernes déroulent déjà les boucles le cas échéant - un exemple étant gcc, si passé les indicateurs d'optimisation, le manuel dit qu'il le fera:

Dérouler des boucles dont le nombre d'itérations peut être déterminé au moment de la compilation ou lors de l'entrée dans la boucle.

Donc, en pratique, il est probable que votre compilateur fera les cas triviaux pour vous. C'est donc à vous de vous assurer que le plus grand nombre possible de vos boucles est facile pour le compilateur afin de déterminer combien d'itérations seront nécessaires.

2
Rich Bradshaw

Essayer sans le savoir n'est pas la façon de le faire.
Ce type prend-il un pourcentage élevé de temps global?

Le déroulage de la boucle ne fait que réduire la surcharge de la boucle d'incrémentation/décrémentation, de comparaison pour la condition d'arrêt et de saut. Si ce que vous faites dans la boucle prend plus de cycles d'instructions que la surcharge de boucle elle-même, vous n'allez pas voir beaucoup d'amélioration en pourcentage.

Voici un exemple de comment obtenir des performances maximales.

1
Mike Dunlavey

Le déroulement de la boucle peut être utile dans des cas spécifiques. Le seul gain n'est pas de sauter certains tests!

Il peut par exemple permettre le remplacement scalaire, l'insertion efficace de prélecture logicielle ... Vous seriez surpris de voir à quel point il peut être utile (vous pouvez facilement obtenir une accélération de 10% sur la plupart des boucles même avec -O3) en déroulant agressivement.

Comme cela a été dit auparavant, cela dépend beaucoup de la boucle et le compilateur et l'expérience sont nécessaires. Il est difficile de faire une règle (ou l'heuristique du compilateur pour le déroulement serait parfaite)

1
Kamchatka

Le déroulement de la boucle dépend entièrement de la taille de votre problème. Cela dépend entièrement de la capacité de votre algorithme à réduire la taille en petits groupes de travail. Ce que vous avez fait ci-dessus ne ressemble pas à ça. Je ne sais pas si une simulation de monte carlo peut même être déroulée.

Un bon scénario pour le déroulement d'une boucle serait la rotation d'une image. Puisque vous pouvez faire pivoter des groupes de travail distincts. Pour que cela fonctionne, vous devez réduire le nombre d'itérations.

0
jwendl

Le déroulement de la boucle est toujours utile s'il y a beaucoup de variables locales à la fois dans et avec la boucle. Pour réutiliser davantage ces registres au lieu d'en enregistrer un pour l'index de boucle.

Dans votre exemple, vous utilisez une petite quantité de variables locales, sans trop utiliser les registres.

La comparaison (à la fin de la boucle) est également un inconvénient majeur si la comparaison est lourde (c'est-à-dire une instruction non_test), surtout si elle dépend d'une fonction externe.

Le déroulement de la boucle permet également d'augmenter la prise de conscience du processeur pour la prédiction de branche, mais cela se produit quand même.

0
LiraNuna