En C, j'ai une tâche où je dois faire la multiplication, l'inversion, la transposition, l'addition etc. etc. avec énorme matrices allouées comme des tableaux bidimensionnels, (tableaux de tableaux).
J'ai trouvé le drapeau gcc -funroll-all-loops
. Si je comprends bien, cela déroulera automatiquement toutes les boucles sans aucun effort du programmeur.
Mes questions:
a) gcc inclut-il ce type d'optimisation avec les différents drapeaux d'optimisation comme -O1
, -O2
etc.?
b) Dois-je utiliser des pragma
dans mon code pour profiter du déroulement des boucles ou les boucles sont-elles identifiées automatiquement?
c) Pourquoi cette option n'est-elle pas activée par défaut si le déroulement augmente les performances?
d) Quels sont les indicateurs d'optimisation gcc recommandés pour compiler le programme de la meilleure façon possible? (Je dois exécuter ce programme optimisé pour une seule famille de CPU, c'est la même chose que la machine sur laquelle je compile le code, en fait j'utilise march=native
et -O2
drapeaux)
ÉDITER
Semble qu'il existe des controverses sur l'utilisation de dérouler qui, dans certains cas, peuvent ralentir les performances. Dans mes situations, il existe différentes méthodes qui effectuent simplement des opérations mathématiques en 2 imbriquées pour des cycles pour des éléments de matrice itérés effectués pour une énorme quantité d'éléments. Dans ce scénario, comment le déroulement pourrait ralentir ou augmenter les performances?
Instructions de pipeline de processeurs modernes. Ils aiment savoir ce qui va suivre et faire toutes sortes d'optimisations fantaisistes basées sur des hypothèses de l'ordre dans lequel les instructions doivent être exécutées.
Mais au bout d'une boucle, il y a deux possibilités! Soit vous remontez au sommet, soit vous continuez. Le processeur fait une supposition éclairée sur ce qui va se passer. Si ça se passe bien, tout va bien. Sinon, il doit rincer le pipeline et caler un peu pendant qu'il se prépare à prendre l'autre branche.
Comme vous pouvez l'imaginer, le déroulement d'une boucle élimine les branches et le potentiel de ces décrochages, surtout dans les cas où les chances sont contre une supposition.
Imaginez une boucle de code qui s'exécute 3 fois, puis continue. Si vous supposez (comme le ferait probablement le processeur) qu'à la fin vous répéterez la boucle. 2/3 du temps, vous aurez raison! 1/3 du temps cependant, vous allez caler.
D'un autre côté, imaginez la même situation, mais le code boucle 3000 fois. Ici, il n'y a probablement qu'un gain 1/3000 du temps de déroulement.
Une partie de la fantaisie du processeur mentionnée ci-dessus implique le chargement des instructions de l'exécutable en mémoire dans le cache d'instructions intégré du processeur (raccourci en cache I). Cela contient une quantité limitée d'instructions auxquelles on peut accéder rapidement, mais qui peut se bloquer lorsque de nouvelles instructions doivent être chargées à partir de la mémoire.
Revenons aux exemples précédents. Supposons qu'une quantité raisonnablement petite de code à l'intérieur de la boucle occupe n
octets de I-cache. Si nous déroulons la boucle, cela prend maintenant n * 3
octets. Un peu plus, mais il conviendra probablement très bien à une seule ligne de cache afin que votre cache fonctionne de manière optimale et n'ait pas besoin de bloquer la lecture à partir de la mémoire principale.
La boucle de 3000, cependant, se déroule pour utiliser un énorme n * 3000
octets de I-cache. Cela va nécessiter plusieurs lectures de la mémoire, et probablement pousser d'autres éléments utiles ailleurs dans le programme hors du cache I.
Comme vous pouvez le voir, le déroulement offre plus d'avantages pour les boucles plus courtes, mais finit par détruire les performances si vous avez l'intention de boucler un grand nombre de fois.
Habituellement, un compilateur intelligent prendra une bonne estimation des boucles à dérouler, mais vous pouvez le forcer si vous êtes sûr que vous connaissez mieux. Comment pouvez-vous mieux vous connaître? La seule façon est de l'essayer dans les deux sens et de comparer les horaires!
L'optimisation prématurée est la racine de tout mal - Donald Knuth
Profil d'abord, optimisez plus tard.
Le déroulement de la boucle ne fonctionne pas si le compilateur ne peut pas prédire le nombre exact d'itérations de la boucle au moment de la compilation (ou au moins prédire une limite supérieure, puis ignorer autant d'itérations que nécessaire). Cela signifie que si la taille de votre matrice est variable, l'indicateur n'aura aucun effet.
Maintenant pour répondre à vos questions:
a) gcc inclut-il ce type d'optimisation avec les différents drapeaux d'optimisation comme -O1, -O2 etc.?
Non, vous devez le définir explicitement car il peut ou non accélérer le code et il rend généralement l'exécutable plus gros.
b) Dois-je utiliser des pragmas dans mon code pour profiter du déroulement des boucles ou les boucles sont-elles identifiées automatiquement?
Pas de pragmas. Avec -funroll-loops
le compilateur décide heuristement des boucles à dérouler. Si vous souhaitez forcer le déroulement, vous pouvez utiliser -funroll-all-loops
, mais cela rend généralement le code plus lent.
c) Pourquoi cette option n'est-elle pas activée par défaut si le déroulement augmente les performances?
Il n'augmente pas toujours les performances! De plus, tout n'est pas une question de performance. Certaines personnes se soucient réellement d'avoir de petits exécutables car ils ont peu de mémoire (voir: systèmes embarqués)
d) Quels sont les indicateurs d'optimisation gcc recommandés pour compiler le programme de la meilleure façon possible? (Je dois exécuter ce programme optimisé pour une seule famille de CPU, c'est la même chose que la machine où je compile le code, en fait j'utilise les drapeaux march = native et -O2)
Il n'y a pas de solution miracle. Vous devrez penser, tester et voir. Il existe en fait un théorème qui déclare qu'aucun compilateur parfait ne peut jamais exister.
Avez-vous profilé votre programme? Le profilage est une compétence très utile pour ces choses.
Source (principalement): https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html
Vous obtenez un fond théorique sur le problème et cela laisse suffisamment d'espace pour deviner ce que vous obtenez dans une vraie course. On dit que l'option n'augmente pas toujours les performances car elle dépend de divers facteurs, par exemple de la mise en œuvre de la boucle, de sa charge/corps et autres.
Chaque code est différent et si vous êtes intéressé à trouver la meilleure solution de performance, il est préférable d'exécuter les deux variantes, de mesurer leurs temps d'exécution et de les comparer.
Regardez cette approche dans la réponse ci-dessous pour avoir une idée de la mesure du temps. En deux mots, il vous suffit d'envelopper votre code dans le cycle qui conduira votre programme à fonctionner en quelques secondes. Comme vous optimisez les boucles elles-mêmes, il est judicieux d'écrire un script Shell, qui exécute votre application plusieurs fois.