web-dev-qa-db-fra.com

Pourquoi une boucle simple est-elle optimisée lorsque la limite est 959 mais pas 960?

Considérons cette simple boucle:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

Si vous compilez avec gcc 7 (snapshot) ou clang (trunk) 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 règle simplement la réponse à 960 sans bouclage.

Cependant, si vous modifiez le code en:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}

L’assemblée produite exécute effectivement 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 la même boucle si vous remplacez float par double est 479. Il en va de même pour gcc et clang à nouveau.

Mise à jour 1

Il s'avère que gcc 7 (snapshot) et clang (trunk) se comportent très différemment. Clang optimise les boucles pour toutes les limites inférieures à 960 pour autant que je sache. D'autre part, gcc est sensible à la valeur exacte et n'a pas de limite supérieure. Par exemple, ne le fait pas optimise la boucle lorsque la limite est 200 (ainsi que de nombreuses autres valeurs), mais le fait lorsque la limite est 202 et 20002 (ainsi que beaucoup d'autres valeurs).

131
eleanora

TL; DR

Par défaut, l'instantané actuel GCC 7 se comporte de manière incohérente, alors que les versions précédentes avaient une limite par défaut due à PARAM_MAX_COMPLETELY_PEEL_TIMES , qui est 16. Il peut être remplacé à partir de la ligne de commande.

La raison d'être de la limite est d'empêcher le déroulement d'une boucle trop agressive, ce qui peut être une épée à double tranchant .

Version GCC <= 6.3.0

L'option d'optimisation pertinente pour GCC est -fpeel-loops , activé indirectement avec le drapeau -Ofast _ (c'est moi qui souligne):

Chaînes de peelw pour lesquelles il existe suffisamment d’informations pour ne pas trop rouler (à partir du retour de profil ou de l’analyse statique ). Il active également le pelage complet des boucles (c'est-à-dire la suppression complète des boucles avec un petit nombre constant 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;
    }

par conséquent 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 plaçant avec max-completely-peeled-insns=n et max-completely-peel-times=n paramètres:

max-completely-peeled-insns

Le nombre maximum d'insns d'une boucle complètement épluchée.

max-completely-peel-times

Le nombre maximum d'itérations d'une boucle convient pour un pelage complet.

Pour en savoir plus sur les insns, vous pouvez vous référer à GCC Internals Manual .

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 devient:

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

Bruit

Je ne suis pas sûr de ce que fait réellement Clang et comment modifier ses limites, mais comme je l'ai observé, vous pouvez le forcer à évaluer la valeur finale en marquant la boucle avec n déroulé du pragma , et le supprimer complètement :

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

résultats en:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret
89
Grzegorz Szpetkowski

Après avoir lu le commentaire de Sulthan, je suppose que:

  1. Le compilateur déroule complètement la boucle si son compteur est constant (et pas trop élevé)

  2. 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: cela générerait trop d'instructions avec 1000), les opérations ne peuvent pas être groupées.

Le compilateur pourrait constater que le déroulement de 1000 instructions équivaut à un seul ajout, mais que les étapes 1 et 2 décrites ci-dessus sont deux optimisations distinctes. les opérations peuvent être groupées (exemple: un appel de fonction ne peut pas être groupé).

Remarque: il s'agit d'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érouler/optimiser; écrivez directement l'opération appropriée dans une instruction.

19
Jean-François Fabre

Très bonne question!

Vous semblez avoir atteint le nombre maximal d'itérations ou d'opérations que le compilateur essaie d'intégrer lors de la simplification du code. Comme indiqué par Grzegorz Szpetkowski, il existe des méthodes spécifiques au compilateur pour ajuster ces limites avec des options pragmas ou en ligne de commande.

Vous pouvez également jouer avec l'explorateur du compilateur Godbolt pour comparer l'impact de différents compilateurs et options sur le code généré: gcc 6.2 et icc 17 toujours dans le code pour 960, alors que clang 3.9 ne le fait pas (avec la configuration par défaut de Godbolt, il s’arrête réellement à 73).

12
chqrlie