0.1f'den 0'a değiştirmek neden performansı 10 kat yavaşlatıyor?


1527

Neden bu kod biraz,

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

Aşağıdaki bitten 10 kat daha hızlı çalıştırın (not edilenler dışında özdeş)?

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

Visual Studio 2010 SP1 ile derlerken. Optimizasyon seviyesi oldu -02ile sse2sağladı. Diğer derleyicilerle test yapmadım.


10
Farkı nasıl ölçtünüz? Derlerken hangi seçenekleri kullandınız?
James Kanze

158
Derleyici neden bu durumda +/- 0'ı düşürmüyor ?!
Michael Dorgan

127
@ Zyx2000 Derleyici bu aptalın yakınında değil. Eğer kullanıp aynı kodu tükürür o LINQPad gösterilerde Önemsiz bir örnek Sökme 0, 0f, 0d, hatta (int)0bir bağlamda bir yerde doubleihtiyaç vardır.
millimoose

14
optimizasyon seviyesi nedir?
Otto Allmendinger

Yanıtlar:


1616

Denormalize kayan nokta dünyasına hoş geldiniz ! Performansa zarar verebilirler !!!

Denormal (veya subnormal) sayılar, kayan nokta gösteriminden sıfıra çok yakın bazı ekstra değerler elde etmek için bir tür hack'tir. Denormalize kayan nokta üzerindeki işlemler, normalleştirilmiş kayan noktadan on kat daha yüzlerce kat daha yavaş olabilir . Bunun nedeni birçok işlemcinin bunları doğrudan işleyememesi ve mikrokod kullanarak bunları yakalayıp çözmesi gerektiğidir.

10.000 tekrardan sonra sayıları yazdırmak varsa, bunların üzerinde olmasına bağlı olarak farklı değerlere yakınsadığıdır göreceksiniz 0veya 0.1kullanılır.

İşte x64 üzerinde derlenmiş test kodu:

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

Çıktı:

#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

İkinci çalışmada sayıların sıfıra ne kadar yakın olduğuna dikkat edin.

Denormalize sayılar genellikle nadirdir ve bu nedenle çoğu işlemci bunları verimli bir şekilde ele almaya çalışmaz.


Kodun başlangıcına ekleyerek denormalleri sıfıra akıtırsak , bunun denormalize sayılarla ilgisi olduğunu göstermek için:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Sonra ile sürüm 0artık 10 kat daha yavaş değildir ve aslında daha hızlı olur. (Bu, kodun SSE etkinken derlenmesini gerektirir.)

Bu, bu garip düşük hassasiyet neredeyse sıfır değerlerini kullanmak yerine, sadece sıfıra yuvarlıyoruz.

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

Sonunda, bunun bir tamsayı veya kayan nokta ile ilgisi yoktur. 0Ya da 0.1fher iki döngü bir kayıt dış depolanır / dönüştürülür. Böylece performans üzerinde hiçbir etkisi yoktur.


100
Hala "+ 0" varsayılan olarak derleyici tarafından optimize değil biraz garip buluyorum. Eğer "+ 0.0f" koysaydı bu olur muydu?
s73v3r

51
@ s73v3r Bu çok iyi bir soru. Şimdi meclise baktığım için, + 0.0foptimize bile edilmiyor . Tahmin etmek zorunda + 0.0fkalsaydım, y[i]bir sinyal NaNya da başka bir şey olsaydı yan etkileri olurdu ... Ama yanılmış olabilirim.
Gizemli

14
Çiftler, birçok durumda, sadece farklı bir sayısal büyüklükte hala aynı problemle karşılaşacaktır. Flush-to-zero, ses uygulamaları için (ve burada ve orada 1e-38 kaybetmeyi göze alabileceğiniz diğerleri için) iyidir, ancak x87 için geçerli olmadığını düşünüyorum. FTZ olmadan, ses uygulamaları için genel düzeltme, sayıları normaliteden uzak tutmak için çok düşük bir genlik (duyulamaz) DC veya veya kare dalga sinyali enjekte etmektir.
Russell Borogove

16
@Isaac çünkü y [i] 0,1 eklemeden önemli ölçüde küçük olduğunda, sayının en önemli basamağı daha yüksek olduğu için hassasiyet kaybı ile sonuçlanır.
Dan Is Fiddling By Firelight

167
@ s73v3r: Kayan nokta negatif 0 olduğundan ve -0f'ye + 0.f eklenmesinin sonucu + 0.f olduğu için + 0.f optimize edilemez. Dolayısıyla, 0.f eklemek bir kimlik işlemi değildir ve optimize edilemez.
Eric Postpischil

415

gccOluşturulan montajda fark kullanılması ve uygulanması sadece bu farkı verir:

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

cvtsi2ssqYavaş gerçekten 10 kez olmak değil.

Görünüşe göre, floatsürüm bellekten yüklenmiş bir XMM kaydı kullanır , sürüm ise intgerçek bir int0 değerini talimatı floatkullanmaya dönüştürür ve cvtsi2ssqçok zaman alır. Gcc'ye geçmek -O3yardımcı olmaz. (gcc sürüm 4.2.1.)

(Kullanma doubleyerine floatolsun, değiştiğini hariç gelmez cvtsi2ssqbir içine cvtsi2sdq.)

Güncelleme

Bazı ekstra testler, bunun mutlaka cvtsi2ssqtalimat olmadığını göstermektedir . Elimine edildiğinde ( yerine a kullanarak int ai=0;float a=ai;ve kullanarak ), hız farkı kalır. Yani @Mysticial haklı, denormalize şamandıralar fark yaratıyor. Bu, ve arasındaki değerleri test ederek görülebilir . Yukarıdaki koddaki dönme noktası , ilmekler aniden 10 kat daha uzun sürdüğünde yaklaşık olarakdır.a000.1f0.00000000000000000000000000000001

Güncelleme << 1

Bu ilginç fenomenin küçük bir görselleştirmesi:

  • Sütun 1: Her yineleme için 2'ye bölünen bir kayan nokta
  • Sütun 2: Bu şamandıranın ikili gösterimi
  • Sütun 3: Bu şamandıranın toplamı 1e7 kez

Denormalizasyon devreye girdiğinde üs (son 9 bit) en düşük değerine değiştiğini açıkça görebilirsiniz. Bu noktada, basit ekleme 20 kat yavaşlar.

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

ARM hakkında eşdeğer bir tartışma Yığın Taşması sorusunda Objective-C? .


27
-O-ffast-mathdüzelmez , ama düzeltir . (Bunu her zaman kullanıyorum, IMO'nun hassas soruna neden olduğu köşe kutuları zaten düzgün tasarlanmış bir programda
açılmamalıdır

Gcc-4.6 ile herhangi bir pozitif optimizasyon seviyesinde dönüşüm yoktur.
Jed

@ leftaroundabout: MXCSR'de -ffast-mathFTZ (sıfıra sıfıra) ve DAZ (denormal sıfırdır) ayarlayan bazı ekstra başlangıç ​​kodlarını içeren bir yürütülebilir (kütüphane değil) derlemek, böylece CPU asla denormaller için yavaş bir mikro kod yardımcısı almak zorunda değildir.
Peter Cordes

34

Denormalize kayan nokta kullanımı nedeniyle. Hem ondan hem de performans cezasından nasıl kurtulurum? Denormal sayıları öldürmenin yolları için internete göz attıktan sonra, bunu yapmanın henüz "en iyi" yolu yok gibi görünüyor. Farklı ortamlarda en iyi sonucu verebilecek bu üç yöntemi buldum:

  • Bazı GCC ortamlarında çalışmayabilir:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
  • Bazı Visual Studio ortamlarında çalışmayabilir: 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)
  • Hem GCC hem de Visual Studio'da çalışıyor gibi görünüyor:

    // 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);
  • Intel derleyici, modern Intel CPU'larda varsayılan olarak denormalleri devre dışı bırakma seçeneklerine sahiptir. Daha fazla ayrıntı burada

  • Derleyici anahtarları. -ffast-math, -msseYa -mfpmath=ssedenormals devre dışı bırakmak ve birkaç diğer şeyler daha hızlı yapmak, ama ne yazık ki aynı zamanda kodunuzu bozabilir diğer yaklaşımlar bir sürü yapacağız. Dikkatlice test edin! Visual Studio derleyicisi için hızlı matematik eşdeğerdir /fp:fastama bunun da denormals devre dışı bırakıp bırakmadığını doğrulamak mümkün değil. 1


1
Bu, farklı ancak ilgili bir soruya iyi bir cevap gibi geliyor (Sayısal hesaplamaların denormal sonuçlar üretmesini nasıl önleyebilirim?) Ancak bu soruya cevap vermiyor.
Ben Voigt

Windows X64, .exe başlatıldığında ani bir alt akış ayarını geçirirken, Windows 32 bit ve linux yapmaz. Linux'ta, gcc -ffast-math ani bir alt akış ayarlamalıdır (ancak Windows'ta değil). Intel derleyicilerinin main () işlevini başlatması gerekir, böylece bu işletim sistemi farklılıkları geçmez, ancak ısırıldım ve programda açıkça ayarlamam gerekiyor. Sandy Bridge ile başlayan Intel CPU'ların toplama / çıkarma işleminden kaynaklanan (ancak bölmediği / çarpmadığı) alt anormallikleri verimli bir şekilde ele alması beklenir, bu nedenle kademeli olarak alt akış kullanmak için bir durum vardır.
tim18

1
Microsoft / fp: fast (varsayılan değil), gcc -ffast-math veya ICL (default) / fp: fast'ta bulunan agresif şeylerden hiçbirini yapmaz. Daha çok ICL / fp: source gibi. Bu derleyicileri karşılaştırmak istiyorsanız açıkça / fp: (ve bazı durumlarda alt akış modunu) ayarlamanız gerekir.
tim18

18

Gcc'de FTZ ve DAZ'ı şu şekilde etkinleştirebilirsiniz:

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

ayrıca gcc anahtarları kullanın: -msse -mfpmath = sse

(Carl Hetherington'a [1] karşılık gelen krediler)

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


Ayrıca bakınız fesetround()dan fenv.hdiğeri için (C99 için tanımlanan), (yuvarlama daha taşınabilir yolu linux.die.net/man/3/fesetround ) (ama bu bütün FP işlemlerini değil, sadece subnormals etkileyecek )
Alman Garcia

FTZ için 1 << 15 ve 1 << 11'e ihtiyacınız olduğundan emin misiniz? Ben sadece 1 << 15 alıntı başka bir yerde gördüm ...
incir

@fig: 1 << 11, Alt Akış Maskesi içindir. Daha fazla bilgi için: softpixel.com/~cwright/programming/simd/sse.php
German Garcia

@GermanGarcia bu OPs sorusuna cevap vermez; soru "Neden bu kod biraz daha hızlı çalışır ..." idi - ya bu geçici çözümü vermeden önce cevaplamaya çalışmalısınız ya da bir yorumda.

9

Dan Neely'nin yorumu bir cevaba genişletilmelidir:

0.0fDenormalize edilen veya yavaşlamaya neden olan sıfır sabiti değil , döngünün her yinelemesinde sıfıra yaklaşan değerlerdir. Sıfıra yaklaştıkça, temsil etmek için daha fazla hassasiyete ihtiyaç duyarlar ve denormalize olurlar. Bunlar y[i]değerler. (Sıfıra yaklaşıyorlar çünkü x[i]/z[i]herkes için 1.0'dan az i.)

Kodun yavaş ve hızlı sürümleri arasındaki önemli fark ifadedir y[i] = y[i] + 0.1f;. Bu çizgi, döngünün her bir yinelemesi gerçekleştirilir yapılmaz, şamandıradaki ekstra hassasiyet kaybolur ve bu hassasiyeti temsil etmek için gereken denormalizasyon artık gerekli değildir. Daha sonra, kayan nokta işlemleri y[i]hızlıdır, çünkü normalleştirilmezler.

Eklediğinizde ekstra hassasiyet neden kayboluyor 0.1f? Çünkü kayan nokta sayıları sadece çok önemli basamaklara sahiptir. Diyelim ki, üç önemli basamak için yeterli depolama alanına sahip olduğunuzu 0.00001 = 1e-5ve 0.00001 + 0.1 = 0.1en azından bu örnek float formatı için yeterli depolama alanına sahip olduğunuzu varsayalım 0.10001.

Kısacası, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;düşündüğünüz no-op değil.

Mistik de bunu söyledi : şamandıraların içeriği sadece montaj kodu değil, önemli.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.