Por que alterar 0,1f para 0 diminui o desempenho em 10x?

1527

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 -02com sse2ativado. Eu não testei com outros compiladores.

Dragarro
fonte
10
Como você mediu a diferença? E que opções você usou quando compilou?
James Kanze
158
Por que o compilador não está descartando apenas +/- 0 neste caso?!?
Michael Dorgan
127
@ Zyx2000 O compilador não é nem de longe tão estúpido. Desmontar um exemplo trivial em mostras LINQPad que ele cospe o mesmo código se você usa 0, 0f, 0d, ou até mesmo (int)0em um contexto em que um doubleé necessário.
Millimoose
14
qual é o nível de otimização?
Otto Allmendinger

Respostas:

1616

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 0ou 0.1não utilizados.

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

Resultado:

#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

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:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Então a versão com 0nã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:

//  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 um ponto flutuante. O 0ou 0.1fé convertido / armazenado em um registro fora dos dois loops. Portanto, isso não afeta o desempenho.

Mysticial
fonte
100
Ainda estou achando um pouco estranho que o "+ 0" não seja completamente otimizado pelo compilador por padrão. Isso teria acontecido se ele tivesse colocado "+ 0.0f"?
S73v3r
51
@ s73v3r Essa é uma pergunta muito boa. Agora que olho para a montagem, nem + 0.0fé otimizado. Se eu tivesse que adivinhar, poderia ser que + 0.0ftivesse efeitos colaterais se y[i]fosse um sinal NaNou algo assim ... Eu poderia estar errado.
Mysticial
14
Os duplos ainda terão o mesmo problema em muitos casos, apenas com uma magnitude numérica diferente. Liberar para zero é bom para aplicativos de áudio (e outros onde você pode perder 1e-38 aqui e ali), mas acredito que não se aplica ao x87. Sem o FTZ, a correção usual para aplicativos de áudio é injetar um sinal DC ou de onda quadrada de amplitude muito baixa (não audível) para afastar os números da desnormalidade.
Russell Borogove
16
@ Isaac, porque quando y [i] é significativamente menor que 0,1, a adição resulta em perda de precisão, pois o dígito mais significativo do número se torna mais alto.
Dan Is Fiddling Por Firelight
167
@ s73v3r: O + 0.f não pode ser otimizado porque o ponto flutuante tem um 0 negativo, e o resultado da adição de + 0.f a -.0f é + 0.f. Portanto, adicionar 0.f não é uma operação de identidade e não pode ser otimizado.
Eric Postpischil
415

Usar gcce aplicar um diff à montagem gerada gera apenas esta diferença:

73c68,69
<   movss   LCPI1_0(%rip), %xmm1
---
>   movabsq $0, %rcx
>   cvtsi2ssq   %rcx, %xmm1
81d76
<   subss   %xmm1, %xmm0

O cvtsi2ssqque é 10 vezes mais lento, de fato.

Aparentemente, a floatversão usa um registro XMM carregado da memória, enquanto a intversão converte um intvalor real 0 para floatusar a cvtsi2ssqinstrução, demorando muito tempo. Passar -O3para o gcc não ajuda. (versão gcc 4.2.1.)

(Usar em doublevez de floatnão importa, exceto que ele muda cvtsi2ssqpara a cvtsi2sdq.)

Atualizar

Alguns testes extras mostram que não é necessariamente a cvtsi2ssqinstrução. Uma vez eliminado (usando ae int ai=0;float a=ai;usando em avez de 0), 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 entre 0e 0.1f. O ponto de virada no código acima é aproximadamente às 0.00000000000000000000000000000001, quando os loops de repente levam 10 vezes mais.

Atualização << 1

Uma pequena visualização desse fenômeno interessante:

  • Coluna 1: um flutuador, dividido por 2 para cada iteração
  • Coluna 2: a representação binária deste flutuador
  • Coluna 3: o tempo necessário para somar esse número 1e7 vezes

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.

0.000000000000000000000000000000000100000004670110: 10111100001101110010000011100000 45 ms
0.000000000000000000000000000000000050000002335055: 10111100001101110010000101100000 43 ms
0.000000000000000000000000000000000025000001167528: 10111100001101110010000001100000 43 ms
0.000000000000000000000000000000000012500000583764: 10111100001101110010000110100000 42 ms
0.000000000000000000000000000000000006250000291882: 10111100001101110010000010100000 48 ms
0.000000000000000000000000000000000003125000145941: 10111100001101110010000100100000 43 ms
0.000000000000000000000000000000000001562500072970: 10111100001101110010000000100000 42 ms
0.000000000000000000000000000000000000781250036485: 10111100001101110010000111000000 42 ms
0.000000000000000000000000000000000000390625018243: 10111100001101110010000011000000 42 ms
0.000000000000000000000000000000000000195312509121: 10111100001101110010000101000000 43 ms
0.000000000000000000000000000000000000097656254561: 10111100001101110010000001000000 42 ms
0.000000000000000000000000000000000000048828127280: 10111100001101110010000110000000 44 ms
0.000000000000000000000000000000000000024414063640: 10111100001101110010000010000000 42 ms
0.000000000000000000000000000000000000012207031820: 10111100001101110010000100000000 42 ms
0.000000000000000000000000000000000000006103515209: 01111000011011100100001000000000 789 ms
0.000000000000000000000000000000000000003051757605: 11110000110111001000010000000000 788 ms
0.000000000000000000000000000000000000001525879503: 00010001101110010000100000000000 788 ms
0.000000000000000000000000000000000000000762939751: 00100011011100100001000000000000 795 ms
0.000000000000000000000000000000000000000381469876: 01000110111001000010000000000000 896 ms
0.000000000000000000000000000000000000000190734938: 10001101110010000100000000000000 813 ms
0.000000000000000000000000000000000000000095366768: 00011011100100001000000000000000 798 ms
0.000000000000000000000000000000000000000047683384: 00110111001000010000000000000000 791 ms
0.000000000000000000000000000000000000000023841692: 01101110010000100000000000000000 802 ms
0.000000000000000000000000000000000000000011920846: 11011100100001000000000000000000 809 ms
0.000000000000000000000000000000000000000005961124: 01111001000010000000000000000000 795 ms
0.000000000000000000000000000000000000000002980562: 11110010000100000000000000000000 835 ms
0.000000000000000000000000000000000000000001490982: 00010100001000000000000000000000 864 ms
0.000000000000000000000000000000000000000000745491: 00101000010000000000000000000000 915 ms
0.000000000000000000000000000000000000000000372745: 01010000100000000000000000000000 918 ms
0.000000000000000000000000000000000000000000186373: 10100001000000000000000000000000 881 ms
0.000000000000000000000000000000000000000000092486: 01000010000000000000000000000000 857 ms
0.000000000000000000000000000000000000000000046243: 10000100000000000000000000000000 861 ms
0.000000000000000000000000000000000000000000022421: 00001000000000000000000000000000 855 ms
0.000000000000000000000000000000000000000000011210: 00010000000000000000000000000000 887 ms
0.000000000000000000000000000000000000000000005605: 00100000000000000000000000000000 799 ms
0.000000000000000000000000000000000000000000002803: 01000000000000000000000000000000 828 ms
0.000000000000000000000000000000000000000000001401: 10000000000000000000000000000000 815 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 44 ms

Uma discussão equivalente sobre o ARM pode ser encontrada na pergunta Stack Overflow, ponto flutuante desnormalizado no Objective-C? .

mvds
fonte
27
-Os não conserta, mas -ffast-mathsim. (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.)
leftaroundabout
Não há conversão em nenhum nível de otimização positivo com o gcc-4.6.
Jed
@leftaroundabout: compilando um executável (não biblioteca) com -ffast-mathlinks, 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.
Peter Cordes
34

É 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:

    // 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 desativar os denormais por padrão nas modernas CPUs Intel. Mais detalhes aqui

  • Opções do compilador. -ffast-math, -msseou -mfpmath=ssedesativará 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:fastmas não consegui confirmar se isso também desabilita os desregulares. 1

FIG
fonte
1
Isso soa como uma resposta decente para uma pergunta diferente, mas relacionada (como posso impedir que cálculos numéricos produzam resultados desnormais?) Mas ela não responde a essa pergunta.
Ben Voigt
O Windows X64 passa uma configuração de sub-fluxo abrupto ao iniciar o .exe, enquanto o Windows de 32 bits e o linux não. No linux, o gcc -ffast-math deve definir um estouro abrupto (mas acho que não no Windows). Os compiladores da Intel devem inicializar em main () para que essas diferenças de SO não passem, mas fui mordido e preciso defini-lo explicitamente no programa. Os processadores Intel iniciados com Sandy Bridge devem lidar com subnormais resultantes de adição / subtração (mas não dividir / multiplicar) de maneira eficiente, portanto, é possível usar subfluxo gradual.
tim18
1
O Microsoft / fp: fast (que não é o padrão) não executa nenhuma das ações agressivas inerentes ao gcc -ffast-math ou ICL (padrão) / fp: fast. É mais como ICL / fp: source. Portanto, você deve definir / fp: (e, em alguns casos, modo de subfluxo) explicitamente se desejar comparar esses compiladores.
tim18
18

No gcc, você pode ativar o FTZ e o DAZ com isso:

#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 opções gcc: -msse -mfpmath = sse

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

[1] http://carlh.net/plugins/denormals.php

Garcia alemão
fonte
Veja também fesetround()a partir de fenv.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 )
German Garcia
Tem certeza de que precisa de 1 << 15 e 1 << 11 para FTZ? Eu só vi 1 << 15 citado em outro lugar ...
fig
@fig: 1 << 11 é para a máscara de subfluxo. Mais informações aqui: softpixel.com/~cwright/programming/simd/sse.php
German Garcia
@GermanGarcia, isso não responde à pergunta dos OPs; a pergunta era "Por que esse código é executado 10 vezes mais rápido que ..." - você deve tentar responder antes de fornecer essa solução alternativa ou fornecer isso em um comentário.
9

O comentário de Dan Neely deve ser expandido para uma resposta:

Não é a constante zero 0.0fque é 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 os y[i]valores. (Eles se aproximam de zero porque x[i]/z[i]é menor que 1,0 para todos 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 y[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 significativos 0.00001 = 1e-5e 0.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 significativo 0.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.

remoções
fonte