Considere este loop simples:
float f(float x[]) {
float p = 1.0;
for (int i = 0; i < 959; i++)
p += 1;
return p;
}
Se você compilar com o gcc 7 (snapshot) ou clang (trunk), -march=core-avx2 -Ofast
obterá algo muito semelhante.
.LCPI0_0:
.long 1148190720 # float 960
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
ret
Em outras palavras, apenas define a resposta para 960 sem repetir.
No entanto, se você alterar o código para:
float f(float x[]) {
float p = 1.0;
for (int i = 0; i < 960; i++)
p += 1;
return p;
}
A montagem produzida realmente executa a soma do loop? Por exemplo, clang dá:
.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
Por que isso e por que é exatamente o mesmo para clang e gcc?
O limite para o mesmo loop se você substituir float
por double
é 479. É o mesmo para gcc e clang novamente.
Atualização 1
Acontece que o gcc 7 (instantâneo) e o clang (tronco) se comportam de maneira muito diferente. clang otimiza os loops para todos os limites inferiores a 960, tanto quanto eu posso dizer. O gcc, por outro lado, é sensível ao valor exato e não tem um limite superior. Por exemplo, ele não otimiza o loop quando o limite é 200 (assim como muitos outros valores), mas o faz quando o limite é 202 e 20002 (assim como muitos outros valores).
fonte
Respostas:
TL; DR
Por padrão, o instantâneo atual GCC 7 se comporta de forma inconsistente, enquanto as versões anteriores têm limite padrão devido a
PARAM_MAX_COMPLETELY_PEEL_TIMES
, que é 16. Ele pode ser substituído na linha de comando.A lógica do limite é impedir o desenrolar excessivo do loop, que pode ser uma faca de dois gumes .
Versão do GCC <= 6.3.0
A opção de otimização relevante para o GCC é
-fpeel-loops
, que é ativada indiretamente junto com o sinalizador-Ofast
(a ênfase é minha):Mais detalhes podem ser obtidos adicionando
-fdump-tree-cunroll
:A mensagem é de
/gcc/tree-ssa-loop-ivcanon.c
:conseqüentemente
try_peel_loop
função retornafalse
.Saída mais detalhada pode ser alcançada com
-fdump-tree-cunroll-details
:É possível ajustar os limites usando
max-completely-peeled-insns=n
emax-completely-peel-times=n
params:Para saber mais sobre insns, você pode consultar Manual interno do GCC .
Por exemplo, se você compilar com as seguintes opções:
então o código se transforma em:
Clang
Não sei ao certo o que o Clang realmente faz e como ajustar seus limites, mas como observei, você pode forçá-lo a avaliar o valor final marcando o loop com pragma de desenrolamento , e ele o removerá completamente:
resulta em:
fonte
PARAM_MAX_COMPLETELY_PEEL_TIMES
parâmetro, que é definido no/gcc/params.def:321
com o valor 16.Depois de ler o comentário de Sulthan, acho que:
O compilador desenrola totalmente o loop se o contador de loop for constante (e não muito alto)
Uma vez desenrolado, o compilador vê que as operações de soma podem ser agrupadas em uma.
Se o loop não for desenrolado por algum motivo (aqui: ele geraria muitas instruções com
1000
), as operações não poderão ser agrupadas.O compilador pode ver que o desenrolar de 1000 instruções equivale a uma única adição, mas as etapas 1 e 2 descritas acima são duas otimizações separadas, portanto, não pode assumir o "risco" de desenrolar, sem saber se as operações podem ser agrupadas (exemplo: uma chamada de função não pode ser agrupada).
Nota: Este é um caso de canto: quem usa um loop para adicionar a mesma coisa novamente? Nesse caso, não confie no compilador possível de desenrolar / otimizar; escreva diretamente a operação correta em uma instrução.
fonte
not too high
parte? Quero dizer, por que o risco não existe em caso de100
? Eu adivinhei algo ... no meu comentário acima .. pode ser a razão para isso?max-unrolled-insns
ao ladomax-unrolled-times
float
para umint
, o compilador gcc poderá reduzir com força o loop, independentemente da contagem de iterações, devido às otimizações das variáveis de indução (-fivopts
). Mas esses não parecem funcionar parafloat
s.Muito boa pergunta!
Você parece ter atingido um limite no número de iterações ou operações que o compilador tenta alinhar ao simplificar o código. Conforme documentado por Grzegorz Szpetkowski, existem maneiras específicas do compilador de ajustar esses limites com pragmas ou opções de linha de comando.
Você também pode jogar com o Compiler Explorer do Godbolt para comparar como diferentes compiladores e opções afetam o código gerado:
gcc 6.2
eicc 17
ainda alinhar o código para o 960, enquantoclang 3.9
isso não afeta (com a configuração padrão do Godbolt, ele na verdade para de inlinhar em 73).fonte