c++ - static variable java




Por que a mudança de 0,1f para 0 reduz o desempenho em 10x? (4)

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; // <--
    }
}

correr mais de 10 vezes mais rápido que o bit seguinte (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. (Eu não testei com outros compiladores.)


É devido ao uso de ponto flutuante desnormalizado. Como se livrar de ambos e a penalidade de desempenho? Tendo vasculhado a Internet em busca de maneiras de matar números denormais, 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 CCG:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
  • Pode não funcionar em alguns ambientes do Visual Studio: 1

    // Requires #include <xmmintrin.h>
    _mm_setcsr( _mm_getcsr() | (1<<15) | (1<<6) );
    // Does both FTZ and DAZ bits. You can also use just hex value 0x8040 to do both.
    // You might also want to use the underflow mask (1<<11)
  • Parece funcionar no GCC e no Visual Studio:

    // Requires #include <xmmintrin.h>
    // Requires #include <pmmintrin.h>
    _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
    _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
  • O compilador Intel tem opções para desabilitar os denormais por padrão nos modernos processadores da Intel. Mais detalhes aqui

  • Switches do compilador. -ffast-math , -msse ou -mfpmath=sse desabilitarão os denormals e farão algumas outras coisas mais rápidas, mas infelizmente também fazem 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 desativa os denormals. 1


No gcc você pode habilitar FTZ e DAZ com isto:

#include <xmmintrin.h>

#define FTZ 1
#define DAZ 1   

void enableFtzDaz()
{
    int mxcsr = _mm_getcsr ();

    if (FTZ) {
            mxcsr |= (1<<15) | (1<<11);
    }

    if (DAZ) {
            mxcsr |= (1<<6);
    }

    _mm_setcsr (mxcsr);
}

use também os switches do gcc: -msse -mfpmath = sse

(créditos correspondentes para Carl Hetherington [1])

[1] 1


O comentário de Dan Neely deveria ser expandido em uma resposta:

Não é a constante zero 0.0f que é desnormalizada ou provoca 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 tornam-se desnormalizados. Estes são os valores y[i] . (Eles se aproximam de zero porque x[i]/z[i] é menor que 1.0 para todos os i .)

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 flutuante em y[i] permanecem rápidas porque não estão desnormalizadas.

Por que a precisão extra é perdida quando você adiciona 0.1f ? Porque os números de ponto flutuante têm apenas tantos dígitos significativos. Digamos que você tenha armazenamento suficiente para três dígitos significativos, então 0.00001 = 1e-5 e 0.00001 + 0.1 = 0.1 , pelo menos para esse formato flutuante de exemplo, porque ele não tem espaço para armazenar o bit menos significativo em 0.10001 .

Em suma, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; não é o não-op que você pode pensar que é.

Mystical também disse isso : o conteúdo dos carros alegóricos é importante, não apenas o código de montagem.


Bem-vindo ao mundo do ponto flutuante desnormalizado ! Eles podem causar estragos no desempenho !!!

Os números de Denormal (ou subnormal) são uma espécie de hack para obter valores muito próximos de zero da representação de ponto flutuante. As operações em ponto flutuante desnormalizado podem ser de dezenas a centenas de vezes mais lentas do que em ponto flutuante normalizado. Isso ocorre porque muitos processadores não podem manipulá-los 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 se 0 ou 0.1 for usado.

Aqui está o código de teste compilado em x64:

int main() {

    double start = omp_get_wtime();

    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];
#ifdef FLOATING
            y[i]=y[i]+0.1f;
            y[i]=y[i]-0.1f;
#else
            y[i]=y[i]+0;
            y[i]=y[i]-0;
#endif

            if (j > 10000)
                cout << y[i] << "  ";
        }
        if (j > 10000)
            cout << endl;
    }

    double end = omp_get_wtime();
    cout << end - start << endl;

    system("pause");
    return 0;
}

Saída:

#define FLOATING
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007

//#define FLOATING
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.46842e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.45208e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044

Note 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 de maneira eficiente.

Para demonstrar que isso tem tudo a ver com os números desnormalizados, se liberarmos os denormais para zero adicionando isso ao início do código:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Em seguida, a versão com 0 não é mais 10x mais lenta e, na verdade, se torna mais rápida. (Isso requer que o código seja compilado com o SSE ativado.)

Isso significa que, em vez de usar esses valores quase zero de precisão mais baixa, acabamos de arredondar para zero.

Horários: Core i7 920 @ 3,5 GHz:

//  Don't flush denormals to zero.
0.1f: 0.564067
0   : 26.7669

//  Flush denormals to zero.
0.1f: 0.587117
0   : 0.341406

No final, isso realmente não tem nada a ver com se é um número inteiro ou ponto flutuante. 0 ou 0.1f é convertido / armazenado em um registrador fora de ambos os loops. Então isso não tem efeito no desempenho.





floating-point