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





optimization flags (4)


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 , 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 de la boucle trop agressif, qui peut être une épée à double tranchant .

Version GCC <= 6.3.0

L'option d'optimisation pertinente pour GCC est -fpeel-loops , qui est activée indirectement avec flag -Ofast (l'accent est à moi):

Peels boucles pour lesquelles il y a suffisamment d'informations qu'ils ne roulent pas beaucoup (à partir de retour de profil ou d'analyse statique ). Il active également le décollement complet de la boucle (c'est-à-dire l' élimination 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 provient 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;
    }

try_peel_loop fonction try_peel_loop renvoie donc false .

Une sortie plus verbeuse 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 des insns 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 maximum d'itérations d'une boucle pour convenir à un pelage complet.

Pour en savoir plus sur les insns, vous pouvez consulter le manuel interne de 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

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 pourriez le forcer à évaluer la valeur finale en marquant la boucle avec dérouler pragma , et il va l'enlever 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

Considérez 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 définit simplement la réponse à 960 sans boucle.

Cependant, si vous changez le code pour:

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 la même boucle si vous remplacez float avec double est 479. C'est la même chose pour gcc et clang encore.

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, il n'optimise pas la boucle lorsque la limite est de 200 (ainsi que de nombreuses autres valeurs) mais elle le fait lorsque la limite est de 202 et 20002 (ainsi que de nombreuses autres valeurs).




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

  1. Le compilateur déroule entièrement la boucle si le compteur de boucle 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 a pu voir que le déroulement de 1000 instructions équivaut à une seule addition, mais les étapes 1 et 2 décrites ci-dessus sont deux optimisations distinctes, donc il ne peut pas prendre le "risque" de dérouler, ne sachant pas si les opérations peuvent être groupées. un appel de fonction ne peut pas être groupé).

Note: C'est un cas de coin: Qui utilise une boucle pour rajouter la même chose? Dans ce cas, ne comptez pas sur le compilateur possible dérouler / optimiser; écrire 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 de mettre en ligne lors de la simplification du code. Comme l'a documenté Grzegorz Szpetkowski, il existe des façons spécifiques au compilateur de 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 les différents compilateurs et options impactant le code généré: gcc 6.2 et icc 17 toujours le code pour 960, contrairement à clang 3.9 (avec la configuration par défaut de Godbolt, il s'arrête à 73 ).




Ne pas dire que c'est une bonne idée, mais vous pouvez certainement écrire du code qui repose sur l'ordre des membres d'une structure. Par exemple, en tant que hack, les gens jettent souvent un pointeur sur une structure comme le type d'un certain champ à l'intérieur duquel ils veulent accéder, puis utilisent l'arithmétique du pointeur pour y arriver. Pour moi c'est une idée assez dangereuse, mais je l'ai vu utilisé, surtout en C ++ pour forcer une variable déclarée privée à être accessible au public quand elle est dans une classe d'une bibliothèque tierce et n'est pas encapsulée publiquement. Réorganiser les membres serait totalement casser cela.







c gcc optimization clang