- Um caso em que foram observadas as diferenças de otimização entre GCC e Clang ao compilar uma função simples de soma de inteiros
- O GCC faz uma otimização de loop no formato
x*2 + 1, somando dois números de uma vez dentro do loop
- O Clang remove completamente o loop e substitui o cálculo por uma fórmula fechada
v(v - 1)/2
- Com essa transformação, o código passa de O(n) para O(1), com simplificação matemática feita automaticamente
- Um exemplo que mostra o nível de otimização inteligente dos compiladores, a ponto de surpreender até um autor com mais de 20 anos de experiência com compiladores
Otimização de loop no GCC
- Ao escrever uma função simples de soma de inteiros, o GCC gera um código de soma eficiente baseado em loop
- Dentro do loop, ele usa a instrução
lea edx, [rdx+1+rax*2] para somar dois números ao mesmo tempo
- Isso corresponde a transformar a operação de somar
x e x+1 em x*2 + 1
- Ao elevar o nível de otimização para
-O3, ele processa o loop mais rapidamente por meio de soma paralela (vectorization)
- Essa abordagem é uma forma tradicional de otimização que mantém o loop enquanto maximiza a eficiência das operações
Otimização matemática no Clang
- Ao compilar o mesmo código com Clang, o loop desaparece completamente
- O Clang verifica se o valor de entrada é positivo e então realiza o cálculo com uma sequência de instruções
lea, imul e shr
- Como resultado, ele converte o cálculo em
(v² - v)/2, isto é, uma fórmula fechada para a soma de inteiros
- Essa transformação elimina o loop e o substitui por um cálculo em tempo constante (O(1))
- O autor descreve esse resultado como “ter encontrado por conta própria a solução matemática para a soma de inteiros”
Processo de desenvolvimento da fórmula
- Ao rastrear matematicamente, em sentido inverso, o código assembly gerado pelo Clang, obtém-se a seguinte transformação
v + ((v - 1)(v - 2) / 2) - 1
- Ao expandir e simplificar, isso se reduz a
(v² - v)/2
- No fim, chega-se à forma
v(v - 1)/2, que coincide com a fórmula da soma de 1 até v
- Esse processo é apresentado como um caso em que o compilador reconheceu um padrão matemático e o otimizou
O comportamento inteligente do compilador
- Explica-se que o motivo de o Clang usar essa sequência específica de instruções está ligado à prevenção de overflow e à forma de rastrear variáveis de indução
- Embora a causa exata do funcionamento interno não esteja totalmente clara, o texto menciona que isso é resultado da ação combinada da lógica interna de otimização do Clang
- O autor descreve esse resultado como “uma experiência humilde e inspiradora”
Encerramento e contexto da série
- Este texto é o 24º artigo da série ‘Advent of Compiler Optimisations 2025’
- Ele explora como os compiladores alcançam simplificação matemática e ganho de desempenho por meio de transformações de código
- O autor enfatiza que, “os compiladores ainda me surpreendem”, destacando o encantamento técnico contínuo mesmo após muitos anos de experiência
1 comentários
Comentários no Hacker News
O código que implementa esse recurso está em ScalarEvolution.cpp e IndVarSimplify.cpp
Esse tipo de otimização é realmente interessante
As otimizações de compilador se dividem em duas grandes categorias — análise de fluxo de dados e reconhecimento e substituição de padrões
A primeira funciona bem para a maioria dos programas, e a segunda é um trabalho de acumular padrões com retorno cada vez menor
O exemplo do link é inteligente e divertido, mas na prática não tem muito valor. Em 45 anos eu nunca escrevi um loop desses
Claro, esse tipo de substituição de padrão ainda importa porque é gerado automaticamente a partir de código de alto nível
Para ser sincero, pode ser só eu resmungando porque meu optimizer não consegue fazer esse tipo de otimização
Isso é um recurso chamado Scalar Evolution (SCEV), e no LLVM ele é implementado de forma bem complexa
Há um caso de otimização parecido no artigo “Do not optimize away”
O realmente incrível dessa otimização é que ela é genérica (generic)
Não é apenas um reconhecimento do padrão “soma de sequência finita de inteiros”; é impressionante que isso seja aplicado de forma geral
Parece que o compilador poderia adicionar mais closed forms. Fico curioso se isso valeria a pena
Um conceito relacionado é o Wilf–Zeilberger pair
Mais uma vez me surpreendi com o resultado de que o GCC é mais lento que o Clang
Mesmo com o GCC tendo 20 anos de vantagem, ainda há casos em que o Clang gera código mais rápido
No passado, ao fazer extração de bits, o Clang resolveu com dois shifts, enquanto o GCC fez três
Fico curioso se alguém já tentou uma otimização que substitui o problema de coloração de grafos (graph coloring) por uma constante
Este é um exemplo que embaralha a fronteira entre implementação e especificação
Achamos que estamos escrevendo uma implementação, mas na prática estamos escrevendo um proxy da especificação
Em outras palavras, o compilador está criando a ilusão de uma máquina imperativa
No começo, achei surpreendente que o Matt não soubesse desse comportamento
Eu também fiquei chocado quando, na época da faculdade, brinquei com LLVM IR e vi recursão ser simplificada em multiplicação
O fato de o Matt não saber pode significar que essa otimização só se aplica a casos simples com os quais ele não lida
Link do vídeo no YouTube