Este artigo tem como objetivo destacar o poder das otimizações de compiladores, com foco nos compiladores Intel C++ — conhecidos por sua popularidade e uso generalizado.
Destaques: O que são otimizações do compilador? | -Ligado | Arquitetura direcionada | Otimização Interprocedural | -fno-aliasing | Relatórios de otimização do compilador
Qualquer compilador executa uma série de etapas para converter o código-fonte de alto nível em código de máquina de baixo nível. Estes envolvem análise lexical, análise de sintaxe, análise semântica, geração de código intermediário (ou IR), otimização e geração de código.
Durante a fase de otimização, o compilador busca meticulosamente maneiras de transformar um programa, visando uma saída semanticamente equivalente que utilize menos recursos ou execute mais rapidamente. As técnicas empregadas neste processo abrangem, mas não estão limitadas a dobramento constante, otimização de loop, inlining de função e eliminação de código morto .
Os desenvolvedores podem especificar um conjunto de sinalizadores do compilador durante o processo de compilação, uma prática familiar para aqueles que usam opções como “ -g” ou “-pg” com GCC para depuração e criação de perfil de informações. À medida que avançamos, discutiremos sinalizadores de compilador semelhantes que podemos usar ao compilar nosso aplicativo com o compilador Intel C++. Isso pode ajudá-lo a melhorar a eficiência e o desempenho do seu código.
você(x,y,t) é a temperatura no ponto (x,y) no tempo t.
Basicamente, temos uma codificação C++ executando as iterações de Jacobi em grades de tamanhos variáveis (que chamamos de resoluções). Basicamente, um tamanho de grade de 500 significa resolver uma matriz de tamanho 500x500 e assim por diante.
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
MFLOP/s significa “Milhões de operações de ponto flutuante por segundo”. É uma unidade de medida usada para quantificar o desempenho de um computador ou processador em termos de operações de ponto flutuante. As operações de ponto flutuante envolvem cálculos matemáticos com números decimais ou reais representados em formato de ponto flutuante.
Nota 1: Para fornecer um resultado estável, executo o executável 5 vezes para cada resolução e pego o valor médio dos valores MFLOP/s.
Nota 2: É importante observar que a otimização padrão no compilador Intel C++ é -O2. Portanto, é importante especificar -O0 ao compilar o código-fonte.
Esses são alguns dos sinalizadores de compilador mais comumente usados quando se começa com otimizações do compilador. Em um caso ideal, o desempenho de Ofast > O3 > O2 > O1 > O0 . No entanto, isso não acontece necessariamente. Os pontos críticos dessas opções são os seguintes:
-O1:
-O2:
-O3:
-Orápido:
É claramente evidente que todas essas otimizações são muito mais rápidas que nosso código base (com “-O0”). O tempo de execução é 2–3x menor que o caso base. E quanto aos MFLOP/s??
No geral, embora apenas ligeiramente, “-O3” tem o melhor desempenho.
Os sinalizadores extras usados por “- Ofast ” (“ -no-prec-div -fp-model fast=2 ”) não estão proporcionando nenhuma aceleração adicional.
A resposta está nos sinalizadores estratégicos do compilador. Experimentar opções como “ -xHost ” e, mais precisamente, “ -xCORE-AVX512 ” pode nos permitir aproveitar todo o potencial dos recursos da máquina e personalizar otimizações para desempenho ideal.
-xHost:
-xCORE-AVX512:
Objetivo: instruir explicitamente o compilador para gerar código que utiliza o conjunto de instruções Intel Advanced Vector Extensions 512 (AVX-512).
Principais recursos: AVX-512 é um conjunto avançado de instruções SIMD (instrução única, dados múltiplos) que oferece registros vetoriais mais amplos e operações adicionais em comparação com versões anteriores, como AVX2. Habilitar esse sinalizador permite que o compilador aproveite esses recursos avançados para otimizar o desempenho.
Considerações: A portabilidade é novamente a culpada aqui. Os binários gerados com instruções AVX-512 podem não funcionar de maneira ideal em processadores que não suportam este conjunto de instruções. Eles podem não funcionar de jeito nenhum!
Por padrão, “ -xCORE-AVX512 ” assume que o programa provavelmente não se beneficiará do uso de registros zmm. O compilador evita usar registros zmm, a menos que seja garantido um ganho de desempenho.
Se alguém planeja usar os registradores zmm sem restrições, “ ” pode ser definido como alto. É isso que faremos também.
Uau!
A parte notável é que alcançamos esses resultados sem quaisquer intervenções manuais substanciais — simplesmente incorporando um punhado de flags do compilador durante o processo de compilação da aplicação.
Nota: Não se preocupe se o seu hardware não suportar AVX-512. O compilador Intel C++ suporta otimizações para AVX, AVX-2 e até mesmo SSE. A tem tudo que você precisa saber!
O IPO é um processo de várias etapas que se concentra nas interações entre diferentes funções ou procedimentos dentro de um programa. O IPO pode incluir muitos tipos diferentes de otimizações, incluindo substituição direta, conversão de chamada indireta e Inlining.
-ipo:
Objetivo: Permite a otimização interprocedural, permitindo ao compilador analisar e otimizar todo o programa, além dos arquivos de origem individuais, durante a compilação.
Principais recursos: - Otimização de todo o programa: “ -ipo ” realiza análise e otimização em todos os arquivos de origem, considerando as interações entre funções e procedimentos em todo o programa. - Otimização entre funções e módulos cruzados: O sinalizador facilita funções inlining, sincronização de otimizações e análise de fluxo de dados em diferentes partes do programa.
Considerações: Requer uma etapa de link separada. Após compilar com “ -ipo ”, uma etapa específica do link é necessária para gerar o executável final. O compilador executa otimizações adicionais com base na visualização completa do programa durante a vinculação.
-ip:
Objetivo: permite a propagação de análise interprocedural, permitindo que o compilador execute algumas otimizações interprocedimentos sem exigir uma etapa de link separada.
Principais recursos: - Análise e propagação: “ -ip ” permite que o compilador realize pesquisas e propagação de dados em diferentes funções e módulos durante a compilação. No entanto, ele não executa todas as otimizações que exigem a visualização completa do programa.- Compilação mais rápida: Ao contrário de “ -ipo ”, “ -ip ” não necessita de uma etapa de vinculação separada, resultando em tempos de compilação mais rápidos. Isso pode ser benéfico durante o desenvolvimento, quando o feedback rápido é essencial.
Considerações: Ocorrem apenas algumas otimizações interprocedimentos limitadas, incluindo inlining de função.
-ipo geralmente fornece recursos de otimização interprocedimento mais extensos, pois envolve uma etapa de link separada, mas tem o custo de tempos de compilação mais longos. [ ] -ip é uma alternativa mais rápida que executa algumas otimizações interprocedimentos sem exigir uma etapa de link separada, tornando-o adequado para fases de desenvolvimento e teste.[ ]
Como estamos falando apenas de desempenho e diferentes otimizações, tempos de compilação ou tamanho do executável não são nossa preocupação, vamos nos concentrar em “ -ipo ”.
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
A função jacobi() leva alguns ponteiros para dobrar como parâmetros e então faz algo dentro dos loops for aninhados. Quando qualquer compilador vê esta função no arquivo fonte, deve ter muito cuidado.
A expressão para calcular unew usando u envolve a média de 4 valores de u vizinhos. E se você e unew apontarem para o mesmo local? Este se tornaria o problema clássico dos ponteiros com alias [ ].
Os compiladores modernos são muito inteligentes e para garantir a segurança, eles assumem que o alias poderia ser possível. E para cenários como esse, evitam quaisquer otimizações que possam impactar a semântica e a saída do código.
No nosso caso, sabemos que u e unew são locais de memória diferentes e destinam-se a armazenar valores diferentes. Portanto, podemos facilmente informar ao compilador que não haverá nenhum alias aqui.
Existem dois métodos. A primeira é a palavra-chave “ ” do C. Mas isso requer alteração do código. Não queremos isso por enquanto.
Algo simples? Vamos tentar “ -fno-alias ”.
-fno-alias:
Objetivo: Instruir o compilador a não assumir alias no programa.
Principais recursos: Supondo que não haja alias, o compilador pode otimizar o código com mais liberdade, melhorando potencialmente o desempenho.
Considerações: O desenvolvedor deve ter cuidado ao usar este sinalizador, pois no caso de qualquer alias injustificado, o programa pode fornecer resultados inesperados.
Bem, agora temos uma coisa!!!
Um exame mais detalhado do código assembly (embora não compartilhado aqui) e do relatório de otimização de compilação gerado (veja abaixo ) revela a aplicação inteligente do compilador de e . Estas transformações contribuem para um desempenho altamente otimizado, mostrando o impacto significativo das diretivas do compilador na eficiência do código.
O compilador Intel C++ fornece um recurso valioso que permite aos usuários gerar um relatório de otimização resumindo todos os ajustes feitos para fins de otimização [ ]. Este relatório abrangente é salvo no formato de arquivo YAML, apresentando uma lista detalhada de otimizações aplicadas pelo compilador dentro do código. Para uma descrição detalhada, consulte a documentação oficial em “ ”.
Da mesma forma, os compiladores Intel C++ (e todos os mais populares) também suportam diretivas pragma, que são recursos muito interessantes. Vale a pena conferir alguns pragmas como ivdep, paralelo, simd, vetor, etc., no .