Esta é uma pergunta que veio à mente ao ler a brilhante resposta de Mysticial à pergunta: por que é mais rápido processar uma matriz classificada do que uma matriz não classificada ?
Contexto para os tipos envolvidos:
const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;
Em sua resposta, ele explica que o Intel Compiler (ICC) otimiza isso:
for (int i = 0; i < 100000; ++i)
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += data[c];
... em algo equivalente a isso:
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
for (int i = 0; i < 100000; ++i)
sum += data[c];
O otimizador está reconhecendo que eles são equivalentes e, portanto, está trocando os loops , movendo o ramo para fora do loop interno. Muito esperto!
Mas por que não faz isso?
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += 100000 * data[c];
Esperemos que Mysticial (ou qualquer outra pessoa) possa dar uma resposta igualmente brilhante. Eu nunca aprendi sobre as otimizações discutidas nessa outra pergunta antes, então estou muito agradecido por isso.
c
performance
compiler-optimization
jhabbott
fonte
fonte
volatile
, o intercâmbio de loop também seria uma otimização inválida.Respostas:
O compilador geralmente não pode transformar
para dentro
porque o último pode levar ao estouro de números inteiros assinados onde o primeiro não. Mesmo com um comportamento de empacotamento garantido para o estouro de números inteiros de complemento de dois assinados, ele alteraria o resultado (se
data[c]
for 30000, o produto se tornaria-1294967296
para osint
s de 32 bits típicos com empacotamento, enquanto 100000 vezes adicionando 30000 asum
, se isso não exceda, aumentesum
em 3000000000). Observe que o mesmo vale para quantidades não assinadas, com números diferentes, o excesso de quantidade100000 * data[c]
normalmente introduziria um módulo de redução2^32
que não deve aparecer no resultado final.Poderia transformá-lo em
embora, como sempre,
long long
seja suficientemente maior queint
.Por que não faz isso, não sei dizer, acho que foi o que Mysticial disse : "aparentemente, ele não executa um passe de colapso de loop após intercâmbio de loop".
Observe que o próprio intercâmbio de loop geralmente não é válido (para números inteiros assinados), pois
pode levar ao estouro, onde
não faria. É kosher aqui, pois a condição garante que todos
data[c]
que adicionados tenham o mesmo sinal; portanto, se um transbordar, os dois terão.Eu não teria muita certeza de que o compilador levasse isso em consideração (@Mysticial, você poderia tentar com uma condição semelhante à
data[c] & 0x80
que pode ser verdadeira para valores positivos e negativos?). Os compiladores fizeram otimizações inválidas (por exemplo, há alguns anos, eu tive uma ICC (11.0, iirc) usando uma conversão assinado de 32 bits-int-dupla no local1.0/n
onden
estava umunsigned int
. Era duas vezes mais rápido que o do gcc Mas errado, muitos valores eram maiores que2^31
, ops.).fonte
ADD.W A6,$A000
, esquecendo que as operações de palavras com registradores de endereço estendem a palavra para 32 bits antes da adição. Demorou algum tempo para solucionar problemas, já que a única coisa que o código fez entre issoADD
e a próxima vez que retirou o A6 da pilha foi restaurar os registros dos chamadores que ele salvou nesse quadro ... #MyArray[0] = 4;
eu poderia verificar o endereço deMyArray
e olhar para esse local antes e depois da declaração executada; isso não mudaria. Código era algo parecidomove.B @A3,#4
e A3 deveria sempre apontar paraMyArray
qualquer momento em que a instrução fosse executada, mas não. Diversão.Esta resposta não se aplica ao caso específico vinculado, mas se aplica ao título da pergunta e pode ser interessante para futuros leitores:
Devido à precisão finita, a adição repetida de ponto flutuante não é equivalente à multiplicação . Considerar:
Demo
fonte
O compilador contém várias passagens que fazem a otimização. Geralmente, em cada passagem, é feita uma otimização nas instruções ou otimizações de loop. No momento, não há modelo que faça uma otimização do corpo do loop com base nos cabeçalhos do loop. Isso é difícil de detectar e menos comum.
A otimização realizada foi o movimento invariante do código. Isso pode ser feito usando um conjunto de técnicas.
fonte
Bem, eu acho que alguns compiladores podem fazer esse tipo de otimização, assumindo que estamos falando sobre aritmética de números inteiros.
Ao mesmo tempo, alguns compiladores podem se recusar a fazê-lo, porque substituir a adição repetitiva pela multiplicação pode alterar o comportamento de estouro do código. Para tipos inteiros não assinados, não deve fazer diferença, pois o comportamento do estouro é totalmente especificado pelo idioma. Mas para os assinados, pode ser (provavelmente não na plataforma de complemento do 2). É verdade que o estouro assinado realmente leva a um comportamento indefinido em C, o que significa que seria perfeitamente bom ignorar completamente a semântica do estouro, mas nem todos os compiladores são corajosos o suficiente para fazer isso. Muitas vezes, recebe muitas críticas da multidão "C é apenas uma linguagem assembly de nível superior". (Lembre-se do que aconteceu quando o GCC introduziu otimizações com base na semântica de alias estrito?)
Historicamente, o GCC tem se mostrado um compilador que tem o necessário para executar etapas tão drásticas, mas outros compiladores podem preferir manter o comportamento "pretendido pelo usuário", mesmo que não seja definido pelo idioma.
fonte
Agora - pelo menos, o clang :
compila com -O1 para
Estouro inteiro não tem nada a ver com isso; se houver um excesso de número inteiro que cause comportamento indefinido, isso poderá ocorrer nos dois casos. Aqui está o mesmo tipo de função usando em
int
vez delong
:compila com -O1 para
fonte
Há uma barreira conceitual para esse tipo de otimização. Os autores do compilador investem muito em redução de força - por exemplo, substituindo multiplicações por adições e trocas. Eles se acostumam a pensar que as multiplicações são ruins. Portanto, um caso em que alguém deveria seguir o outro caminho é surpreendente e contra-intuitivo. Então, ninguém pensa em implementá-lo.
fonte
As pessoas que desenvolvem e mantêm compiladores têm uma quantidade limitada de tempo e energia para gastar em seu trabalho; portanto, geralmente desejam se concentrar no que seus usuários mais gostam: transformar código bem escrito em código rápido. Eles não querem gastar seu tempo tentando encontrar maneiras de transformar código tolo em código rápido - é para isso que serve a revisão de código. Em uma linguagem de alto nível, pode haver um código "bobo" que expressa uma idéia importante, valendo a pena o tempo dos desenvolvedores para fazer isso rapidamente - por exemplo, o desmatamento de atalho e a fusão de fluxos permitem que os programas Haskell estruturem em torno de certos tipos de preguiçoso estruturas de dados produzidas para serem compiladas em loops estreitos que não alocam memória. Mas esse tipo de incentivo simplesmente não se aplica a transformar a adição em loop em multiplicação. Se você quer que seja rápido,
fonte