Pourquoi une boucle simple est-elle optimisée lorsque la limite est 959 mais pas 960?
Considérez cette boucle simple:
float f(float x[]) {
float p = 1.0;
for (int i = 0; i < 959; i++)
p += 1;
return p;
}
Si vous compiler avec gcc 7 (instantané) ou clang (tronc) avec -march=core-avx2 -Ofast
vous obtenez quelque chose de très similaire.
.LCPI0_0:
.long 1148190720 # float 960
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
ret
En d'autres termes, il définit simplement la réponse à 960 sans boucle.
Cependant, si vous changez le code en:
float f(float x[]) {
float p = 1.0;
for (int i = 0; i < 960; i++)
p += 1;
return p;
}
L'assemblage produit effectue réellement la somme de boucle? Par exemple clang donne:
.LCPI0_0:
.long 1065353216 # float 1
.LCPI0_1:
.long 1086324736 # float 6
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
vxorps ymm1, ymm1, ymm1
mov eax, 960
vbroadcastss ymm2, dword ptr [rip + .LCPI0_1]
vxorps ymm3, ymm3, ymm3
vxorps ymm4, ymm4, ymm4
.LBB0_1: # =>This Inner Loop Header: Depth=1
vaddps ymm0, ymm0, ymm2
vaddps ymm1, ymm1, ymm2
vaddps ymm3, ymm3, ymm2
vaddps ymm4, ymm4, ymm2
add eax, -192
jne .LBB0_1
vaddps ymm0, ymm1, ymm0
vaddps ymm0, ymm3, ymm0
vaddps ymm0, ymm4, ymm0
vextractf128 xmm1, ymm0, 1
vaddps ymm0, ymm0, ymm1
vpermilpd xmm1, xmm0, 1 # xmm1 = xmm0[1,0]
vaddps ymm0, ymm0, ymm1
vhaddps ymm0, ymm0, ymm0
vzeroupper
ret
Pourquoi est-ce et pourquoi est-ce exactement la même chose pour clang et gcc?
La limite pour le même boucle si vous remplacez float
par double
est 479. C'est la même chose pour gcc et clang à nouveau.
Mise à Jour 1
, Il s'avère que gcc 7 (instantané) et clang (tronc) se comportent très différemment. clang optimise les boucles pour toutes les limites inférieures à 960 pour autant que je sache. gcc d'autre part est sensible à la valeur exacte et n'a pas de limite supérieure . Par exemple, il ne optimiser la boucle lorsque la limite est de 200 (ainsi que de nombreux autres valeurs), mais il fait lorsque la limite est 202 et 20002 (ainsi que de nombreuses autres valeurs).
3 réponses
TL; DR
Par défaut, L'instantané actuel GCC 7 se comporte de manière incohérente, alors que les versions précédentes ont une limite par défaut en raison de PARAM_MAX_COMPLETELY_PEEL_TIMES
, soit 16. Il peut être remplacé à partir de la ligne de commande.
La raison d'être de la limite est d'empêcher un déroulement trop agressif de la boucle, qui peut être une épée à double tranchant .
Version GCC
L'option d'optimisation pertinente pour GCC est -fpeel-loops
, qui est activé indirectement avec flag -Ofast
(l'accent est le mien):
Peelings boucles pour lesquelles il y a suffisamment d'informations qu'ils ne le font pas rouler beaucoup (à partir de commentaires de profil ou analyse statique). Il s'allume aussi peeling complet de la boucle (c.-à-d. élimination complète des boucles avec petit constante nombre d'itérations).
Activé avec
-O3
et/ou-fprofile-use
.
Plus de détails peuvent être obtenus en ajoutant -fdump-tree-cunroll
:
$ head test.c.151t.cunroll
;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)
Not peeling: upper bound is known so can unroll completely
Le message est de /gcc/tree-ssa-loop-ivcanon.c
:
if (maxiter >= 0 && maxiter <= npeel)
{
if (dump_file)
fprintf (dump_file, "Not peeling: upper bound is known so can "
"unroll completely\n");
return false;
}
D'Où try_peel_loop
la fonction retourne false
.
Une sortie plus détaillée peut être atteinte avec -fdump-tree-cunroll-details
:
Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely
Il est possible de modifier les limites en plaidant avec max-completely-peeled-insns=n
et max-completely-peel-times=n
params:
max-completely-peeled-insns
Le nombre maximum d'insns d'une boucle complètement pelée.
max-completely-peel-times
Le nombre maximal d'itérations d'une boucle pour être peeling.
Pour en savoir plus sur insns, vous pouvez reportez-vous au manuel interne GCC.
Par exemple, si vous compilez avec les options suivantes:
-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000
Alors le code se transforme en:
f:
vmovss xmm0, DWORD PTR .LC0[rip]
ret
.LC0:
.long 1148207104
Clang
Je ne suis pas sûr de ce que Clang fait réellement et comment modifier ses limites, mais comme je l'ai observé, vous pouvez le forcer à évaluer la valeur finale en marquant la boucle avec unroll pragma , et il le supprimera complètement:
#pragma unroll
for (int i = 0; i < 960; i++)
p++;
Résultats dans:
.LCPI0_0:
.long 1148207104 # float 961
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
ret
Après avoir lu le commentaire de Sulthan, je suppose que:
-
Le compilateur déroule complètement la boucle si le compteur de boucle est constant (et pas trop élevé)
Une fois qu'il est déroulé, le compilateur voit que les opérations de somme peuvent être regroupées en une seule.
Si la boucle n'est pas déroulée pour une raison quelconque (ici: elle générerait trop d'instructions avec 1000
), les opérations ne peuvent pas être regroupées.
Le compilateur peut voir que le dérouleur de 1000 les instructions représentent une seule addition, mais les étapes 1 et 2 décrites ci-dessus sont deux optimisations distinctes, donc elles ne peuvent pas prendre le "risque" de dérouler, ne sachant pas si les opérations peuvent être groupées (exemple: un appel de fonction ne peut pas être groupé).
Remarque: Ceci est un cas de coin: Qui utilise une boucle pour ajouter la même chose encore? Dans ce cas, ne comptez pas sur le compilateur possible déroulez / optimisez; écrivez directement le bon fonctionnement dans une instruction.
Très bonne question!
Vous semblez avoir atteint une limite sur le nombre d'itérations ou d'opérations que le compilateur essaie d'intégrer lors de la simplification du code. Comme documenté par Grzegorz Szpetkowski, il existe des moyens spécifiques au compilateur pour modifier ces limites avec des pragmas ou des options de ligne de commande.
Vous pouvez également jouer avec L'Explorateur de compilateur de Godbolt pour comparer l'impact des différents compilateurs et options sur le code généré: gcc 6.2
et icc 17
toujours en ligne le code pour 960, alors que clang 3.9
ne le fait pas (avec la configuration Godbolt par défaut, il s'arrête en fait à 73).