Eu estive traçando um perfil de alguns de nossos cálculos matemáticos básicos em um Intel Core Duo e, ao examinar várias abordagens para a raiz quadrada, notei algo estranho: usando as operações escalares SSE, é mais rápido obter uma raiz quadrada recíproca e multiplicá-la para obter o sqrt, do que usar o opcode nativo sqrt!
Estou testando com um loop parecido com:
inline float TestSqrtFunction( float in );
void TestFunc()
{
#define ARRAYSIZE 4096
#define NUMITERS 16386
float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache
cyclecounter.Start();
for ( int i = 0 ; i < NUMITERS ; ++i )
for ( int j = 0 ; j < ARRAYSIZE ; ++j )
{
flOut[j] = TestSqrtFunction( flIn[j] );
// unrolling this loop makes no difference -- I tested it.
}
cyclecounter.Stop();
printf( "%d loops over %d floats took %.3f milliseconds",
NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}
Eu tentei isso com alguns corpos diferentes para TestSqrtFunction, e eu tenho alguns tempos que estão realmente coçando minha cabeça. O pior de tudo, de longe, foi usar a função nativa sqrt () e deixar o compilador "inteligente" "otimizar". A 24 ns / flutuante, usar o FPU x87 era pateticamente ruim:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
A próxima coisa que tentei foi usar um intrínseco para forçar o compilador a usar o opcode sqrt escalar do SSE:
inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
_mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
// compiles to movss, sqrtss, movss
}
Isso era melhor, a 11,9 ns / float. Eu também tentei a técnica de aproximação Newton-Raphson maluca do Carmack , que funcionou ainda melhor do que o hardware, a 4,3 ns / float, embora com um erro de 1 em 2 10 (o que é demais para meus propósitos).
A surpresa foi quando tentei a operação de SSE para raiz quadrada recíproca e, em seguida, usei uma multiplicação para obter a raiz quadrada (x * 1 / √x = √x). Mesmo que isso leve duas operações dependentes, foi a solução mais rápida de longe, a 1,24 ns / flutuante e com precisão de 2 -14 :
inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
__m128 in = _mm_load_ss( pIn );
_mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
// compiles to movss, movaps, rsqrtss, mulss, movss
}
Minha pergunta é basicamente o que dá ? Por que o opcode de raiz quadrada embutido no hardware do SSE é mais lento do que sintetizá-lo a partir de duas outras operações matemáticas?
Tenho certeza de que este é realmente o custo da operação em si, porque verifiquei:
- Todos os dados cabem no cache e os acessos são sequenciais
- as funções são embutidas
- desenrolar o loop não faz diferença
- os sinalizadores do compilador estão definidos para otimização total (e a montagem está boa, eu verifiquei)
( editar : stephentyrone aponta corretamente que as operações em longas sequências de números devem usar as operações empacotadas SIMD de vetorização, como rsqrtps
- mas a estrutura de dados da matriz aqui é apenas para fins de teste: o que estou realmente tentando medir é o desempenho escalar para uso no código que não pode ser vetorizado.)
inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }
,. Mas esta é uma má ideia porque pode facilmente induzir um bloqueio de carga-acerto-armazenamento se a CPU grava os flutuadores na pilha e os lê de volta imediatamente - fazendo malabarismo do registrador vetorial para um registrador flutuante para o valor de retorno em particular são más notícias. Além disso, os opcodes subjacentes da máquina que os intrínsecos SSE representam levam operandos de endereço de qualquer maneira.
eax
) é muito ruim, enquanto uma viagem de ida e volta entre xmm0 e pilha e de volta não, por causa do encaminhamento da Intel para as lojas. Você mesmo pode cronometrar para ver com certeza. Geralmente, a maneira mais fácil de ver o potencial LHS é olhar o conjunto emitido e ver onde os dados são manipulados entre os conjuntos de registros; seu compilador pode fazer a coisa certa ou não. Quanto aos vetores normalizando, escrevi os meus resultados aqui: bit.ly/9W5zoU