Tenho tentado otimizar alguns códigos extremamente críticos para o desempenho (um algoritmo de classificação rápida que está sendo chamado milhões e milhões de vezes dentro de uma simulação de monte carlo) por meio do desenrolamento de loop. Aqui está o loop interno que estou tentando acelerar:
// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}
Tentei desenrolar para algo como:
while(true) {
if(myArray[++index1] < pivot) break;
if(myArray[++index1] < pivot) break;
// More unrolling
}
while(true) {
if(pivot < myArray[--index2]) break;
if(pivot < myArray[--index2]) break;
// More unrolling
}
Isso não fez absolutamente nenhuma diferença, então mudei de volta para a forma mais legível. Tive experiências semelhantes outras vezes em que tentei desenrolar loop. Dada a qualidade dos preditores de branch no hardware moderno, quando, se alguma vez, o desenrolamento de loop ainda é uma otimização útil?
Respostas:
O desenrolamento do loop faz sentido se você pode quebrar as cadeias de dependência. Isso dá a uma CPU fora de ordem ou superescalar a possibilidade de programar melhor as coisas e, assim, funcionar mais rápido.
Um exemplo simples:
Aqui, a cadeia de dependência dos argumentos é muito curta. Se você travar porque tem um cache-miss no data-array, a CPU não pode fazer nada além de esperar.
Por outro lado, este código:
poderia correr mais rápido. Se você obtiver uma perda de cache ou outro bloqueio em um cálculo, ainda existem três outras cadeias de dependências que não dependem do bloqueio. Uma CPU fora de serviço pode executá-los.
fonte
Isso não faria nenhuma diferença porque você está fazendo o mesmo número de comparações. Aqui está um exemplo melhor. Ao invés de:
escrever:
Mesmo assim, quase certamente não fará diferença, mas agora você está fazendo 50 comparações em vez de 200 (imagine que a comparação seja mais complexa).
No entanto, o desenrolamento manual de loop em geral é em grande parte um artefato da história. É mais uma da lista crescente de coisas que um bom compilador fará por você quando for importante. Por exemplo, a maioria das pessoas não se preocupa em escrever
x <<= 1
ou emx += x
vez de escreverx *= 2
. Você apenas escrevex *= 2
e o compilador irá otimizá-lo para você da maneira que for melhor.Basicamente, há cada vez menos necessidade de adivinhar seu compilador.
fonte
Independentemente da previsão de branch em hardware moderno, a maioria dos compiladores faz o desenrolamento de loop para você.
Valeria a pena descobrir quantas otimizações seu compilador faz por você.
Achei a apresentação de Felix von Leitner muito esclarecedora sobre o assunto. Eu recomendo que você leia. Resumo: Compiladores modernos são MUITO inteligentes, então otimizações manuais quase nunca são eficazes.
fonte
Pelo que eu entendi, os compiladores modernos já desenrolam loops quando apropriado - um exemplo sendo o gcc, se passado os sinalizadores de otimização, o manual diz que irá:
Portanto, na prática, é provável que seu compilador faça os casos triviais para você. Cabe a você, portanto, certificar-se de que o máximo possível de seus loops seja fácil para o compilador determinar quantas iterações serão necessárias.
fonte
O desenrolamento do loop, seja desenrolamento manual ou desenrolamento do compilador, pode muitas vezes ser contraproducente, particularmente com CPUs x86 mais recentes (Core 2, Core i7). Resumindo: compare seu código com e sem desenrolar de loop em quaisquer CPUs nas quais você planeja implantar esse código.
fonte
Tentar sem saber não é a maneira de o fazer.
Esse tipo de trabalho ocupa uma alta porcentagem do tempo total?
Tudo o que o desenrolamento de loop faz é reduzir a sobrecarga de loop de incremento / decremento, comparação para a condição de parada e salto. Se o que você está fazendo no loop leva mais ciclos de instrução do que o próprio overhead do loop, você não verá muitas melhorias em termos percentuais.
Aqui está um exemplo de como obter desempenho máximo.
fonte
O desenrolamento do loop pode ser útil em casos específicos. O único ganho não é pular alguns testes!
Ele pode, por exemplo, permitir substituição escalar, inserção eficiente de pré-busca de software ... Você ficaria surpreso com o quão útil pode ser (você pode facilmente obter 10% de aceleração na maioria dos loops mesmo com -O3) por meio de um desenrolamento agressivo.
Porém, como foi dito antes, depende muito do loop e do compilador e da experiência são necessários. É difícil fazer uma regra (ou a heurística do compilador para desenrolar seria perfeita)
fonte
O desenrolamento do loop depende inteiramente do tamanho do problema. É totalmente dependente de seu algoritmo ser capaz de reduzir o tamanho em grupos menores de trabalho. O que você fez acima não se parece com isso. Não tenho certeza se uma simulação de monte carlo pode ser desenrolada.
Um bom cenário para o desenrolamento do loop seria girar uma imagem. Já que você pode rodar grupos separados de trabalho. Para fazer isso funcionar, você teria que reduzir o número de iterações.
fonte
O desenrolamento do loop ainda é útil se houver muitas variáveis locais dentro e com o loop. Para reutilizar mais esses registros, em vez de salvar um para o índice de loop.
Em seu exemplo, você usa uma pequena quantidade de variáveis locais, não usando demais os registradores.
A comparação (para o final do loop) também é uma grande desvantagem se a comparação for pesada (ou seja, sem
test
instrução), especialmente se depender de uma função externa.O desenrolamento do loop também ajuda a aumentar a percepção da CPU para a previsão de ramificações, mas essas ocorrem mesmo assim.
fonte