Por que esse pedaço de código,
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0.1f; // <--
y[i] = y[i] - 0.1f; // <--
}
}
executar mais de 10 vezes mais rápido que o bit a seguir (idêntico, exceto onde indicado)?
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0; // <--
y[i] = y[i] - 0; // <--
}
}
ao compilar com o Visual Studio 2010 SP1. O nível de otimização estava -02
com sse2
ativado. Eu não testei com outros compiladores.
0
,0f
,0d
, ou até mesmo(int)0
em um contexto em que umdouble
é necessário.Respostas:
Bem-vindo ao mundo do ponto flutuante desnormalizado ! Eles podem causar estragos no desempenho !!!
Números desnormais (ou subnormais) são uma espécie de hack para obter alguns valores extras muito próximos de zero da representação de ponto flutuante. As operações no ponto flutuante desnormalizado podem ser dezenas a centenas de vezes mais lentas que no ponto flutuante normalizado. Isso ocorre porque muitos processadores não podem lidar com eles diretamente e devem interceptá-los e resolvê-los usando o microcódigo.
Se você imprimir os números após 10.000 iterações, verá que eles convergiram para valores diferentes, dependendo de serem
0
ou0.1
não utilizados.Aqui está o código de teste compilado em x64:
Resultado:
Observe como na segunda execução os números estão muito próximos de zero.
Números desnormalizados são geralmente raros e, portanto, a maioria dos processadores não tenta lidar com eles com eficiência.
Para demonstrar que isso tem tudo a ver com números desnormalizados, se colocarmos desnormalizados em zero , adicionando isso ao início do código:
Então a versão com
0
não é mais 10x mais lenta e, na verdade, fica mais rápida. (Isso requer que o código seja compilado com o SSE ativado.)Isso significa que, em vez de usar esses valores esquisitos de baixa precisão quase zero, arredondamos para zero.
Tempos: Core i7 920 a 3,5 GHz:
No final, isso realmente não tem nada a ver com se é um número inteiro ou um ponto flutuante. O
0
ou0.1f
é convertido / armazenado em um registro fora dos dois loops. Portanto, isso não afeta o desempenho.fonte
+ 0.0f
é otimizado. Se eu tivesse que adivinhar, poderia ser que+ 0.0f
tivesse efeitos colaterais sey[i]
fosse um sinalNaN
ou algo assim ... Eu poderia estar errado.Usar
gcc
e aplicar um diff à montagem gerada gera apenas esta diferença:O
cvtsi2ssq
que é 10 vezes mais lento, de fato.Aparentemente, a
float
versão usa um registro XMM carregado da memória, enquanto aint
versão converte umint
valor real 0 parafloat
usar acvtsi2ssq
instrução, demorando muito tempo. Passar-O3
para o gcc não ajuda. (versão gcc 4.2.1.)(Usar em
double
vez defloat
não importa, exceto que ele mudacvtsi2ssq
para acvtsi2sdq
.)Atualizar
Alguns testes extras mostram que não é necessariamente a
cvtsi2ssq
instrução. Uma vez eliminado (usando aeint ai=0;float a=ai;
usando ema
vez de0
), a diferença de velocidade permanece. Então, @Mysticial está certo, os carros alegóricos desnormalizados fazem a diferença. Isso pode ser visto testando valores entre0
e0.1f
. O ponto de virada no código acima é aproximadamente às0.00000000000000000000000000000001
, quando os loops de repente levam 10 vezes mais.Atualização << 1
Uma pequena visualização desse fenômeno interessante:
Você pode ver claramente o expoente (os últimos 9 bits) mudar para seu valor mais baixo, quando a desnormalização é configurada. Nesse ponto, a adição simples se torna 20 vezes mais lenta.
Uma discussão equivalente sobre o ARM pode ser encontrada na pergunta Stack Overflow, ponto flutuante desnormalizado no Objective-C? .
fonte
-O
s não conserta, mas-ffast-math
sim. (Eu uso isso o tempo todo, IMO os casos de canto onde ele causa problemas de precisão não deve transformar-se em um programa bem concebido de qualquer maneira.)-ffast-math
links, algum código extra de inicialização que define FTZ (nivelado a zero) e DAZ (denormal são zero) no MXCSR, para que a CPU nunca precise prestar assistência lenta ao microcódigo para denormals.É devido ao uso de ponto flutuante desnormalizado. Como se livrar dele e da penalidade de desempenho? Tendo vasculhado a Internet em busca de maneiras de matar números desnormais, parece que ainda não existe uma "melhor" maneira de fazer isso. Eu encontrei esses três métodos que podem funcionar melhor em diferentes ambientes:
Pode não funcionar em alguns ambientes do GCC:
Pode não funcionar em alguns ambientes do Visual Studio: 1
Parece funcionar no GCC e no Visual Studio:
O compilador Intel tem opções para desativar os denormais por padrão nas modernas CPUs Intel. Mais detalhes aqui
Opções do compilador.
-ffast-math
,-msse
ou-mfpmath=sse
desativará as formas anormais e agilizará algumas outras coisas, mas infelizmente também faz muitas outras aproximações que podem quebrar seu código. Teste com cuidado! O equivalente de matemática rápida para o compilador do Visual Studio é,/fp:fast
mas não consegui confirmar se isso também desabilita os desregulares. 1fonte
No gcc, você pode ativar o FTZ e o DAZ com isso:
use também opções gcc: -msse -mfpmath = sse
(créditos correspondentes a Carl Hetherington [1])
[1] http://carlh.net/plugins/denormals.php
fonte
fesetround()
a partir defenv.h
(definido para C99) para outro, muito mais portátil do arredondamento ( linux.die.net/man/3/fesetround ) (mas isso afetaria todas as operações de FP, não apenas subnormais )O comentário de Dan Neely deve ser expandido para uma resposta:
Não é a constante zero
0.0f
que é desnormalizada ou causa uma desaceleração, são os valores que se aproximam de zero a cada iteração do loop. À medida que se aproximam cada vez mais de zero, precisam de mais precisão para representar e se tornam desnormalizados. Estes são osy[i]
valores. (Eles se aproximam de zero porquex[i]/z[i]
é menor que 1,0 para todosi
.)A diferença crucial entre as versões lenta e rápida do código é a declaração
y[i] = y[i] + 0.1f;
. Assim que essa linha é executada a cada iteração do loop, a precisão extra no float é perdida e a desnormalização necessária para representar essa precisão não é mais necessária. Posteriormente, as operações de ponto flutuantey[i]
permanecem rápidas porque não são desnormalizadas.Por que a precisão extra é perdida quando você adiciona
0.1f
? Como os números de ponto flutuante possuem apenas dígitos significativos. Digamos que você tenha armazenamento suficiente para três dígitos significativos0.00001 = 1e-5
e0.00001 + 0.1 = 0.1
, pelo menos para este exemplo de formato flutuante, porque ele não tem espaço para armazenar o bit menos significativo0.10001
.Em suma,
y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
não é o não-op que você pode pensar.Mystical também disse isso : o conteúdo dos carros alegóricos é importante, não apenas o código de montagem.
fonte