Pertanyaan Mengapa mengubah 0.1f ke 0 memperlambat kinerja sebesar 10x?


Mengapa ini sedikit kode,

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

menjalankan lebih dari 10 kali lebih cepat daripada bit berikut (identik kecuali jika dicatat)?

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

saat mengkompilasi dengan Visual Studio 2010 SP1. (Saya belum menguji dengan compiler lain.)


1359
2018-02-16 15:58


asal


Jawaban:


Selamat datang di dunia denormalized floating-point! Mereka bisa mendatangkan malapetaka pada kinerja !!!

Angka-angka Denormal (atau subnormal) adalah semacam peretasan untuk mendapatkan beberapa nilai ekstra yang sangat dekat dengan nol dari representasi floating point. Operasi pada floating-point dapat dinormalkan puluhan hingga ratusan kali lebih lambat dari pada floating-point yang dinormalisasi. Ini karena banyak prosesor tidak dapat menangani mereka secara langsung dan harus menjebak dan menyelesaikannya menggunakan microcode.

Jika Anda mencetak angka setelah 10.000 iterasi, Anda akan melihat bahwa mereka telah menyatu ke nilai yang berbeda tergantung pada apakah 0 atau 0.1 digunakan.

Berikut ini kode uji yang dikompilasi pada 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;
}

Keluaran:

#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

Perhatikan bagaimana dalam menjalankan kedua angka sangat dekat dengan nol.

Angka denormalized umumnya langka dan dengan demikian sebagian besar prosesor tidak mencoba untuk menangani mereka secara efisien.


Untuk menunjukkan bahwa ini semuanya berkaitan dengan angka denormalized, jika kita siram bungkusan ke nol dengan menambahkan ini ke awal kode:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Kemudian versi dengan 0 tidak lagi 10x lebih lambat dan benar-benar menjadi lebih cepat. (Ini mengharuskan kode dikompilasi dengan SSE yang diaktifkan.)

Ini berarti bahwa alih-alih menggunakan nilai presisi hampir nol yang aneh ini, kita hanya membulatkan ke nol sebagai gantinya.

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

Pada akhirnya, ini benar-benar tidak ada hubungannya dengan apakah itu integer atau floating-point. Itu 0 atau 0.1f diubah / disimpan ke dalam register di luar kedua loop. Jadi itu tidak berpengaruh pada kinerja.


1470
2018-02-16 16:20



Menggunakan gcc dan menerapkan diff ke perakitan yang dihasilkan hanya menghasilkan perbedaan ini:

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

Itu cvtsi2ssq satu yang 10 kali lebih lambat memang.

Ternyata, itu float versi menggunakan XMM mendaftar dimuat dari memori, sedangkan int versi mengkonversi nyata int nilai 0 hingga float menggunakan cvtsi2ssq instruksi, mengambil banyak waktu. Lewat -O3 ke gcc tidak membantu. (Versi gcc 4.2.1.)

(Menggunakan double dari pada float tidak masalah, kecuali bahwa itu mengubah cvtsi2ssq menjadi cvtsi2sdq.)

Memperbarui 

Beberapa tes tambahan menunjukkan bahwa itu belum tentu cvtsi2ssq petunjuk. Setelah dihilangkan (menggunakan int ai=0;float a=ai; dan menggunakan a dari pada 0), perbedaan kecepatan tetap. Jadi @Mysticial benar, peleburan denormalized membuat perbedaan. Ini bisa dilihat dengan menguji nilai antara 0 dan 0.1f. Titik balik dalam kode di atas kira-kira sama 0.00000000000000000000000000000001, ketika loop tiba-tiba membutuhkan waktu 10 kali lebih lama.

Perbarui << 1 

Visualisasi kecil dari fenomena yang menarik ini:

  • Kolom 1: pelampung, dibagi 2 untuk setiap iterasi
  • Kolom 2: representasi biner dari pelampung ini
  • Kolom 3: waktu yang dibutuhkan untuk menjumlahkan float ini 1e7 kali

Anda dapat dengan jelas melihat perubahan eksponen (9 bit terakhir) ke nilai terendahnya, ketika denormalization masuk. Pada saat itu, penambahan sederhana menjadi 20 kali lebih lambat.

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

Diskusi setara tentang ARM dapat ditemukan dalam pertanyaan Stack Overflow Denormalized floating point di Objective-C?.


399
2018-02-16 16:19



Ini karena denormalized floating-point use. Bagaimana menyingkirkan keduanya dan penalti kinerja? Setelah menjelajahi Internet untuk cara-cara membunuh angka-angka denormal, tampaknya tidak ada cara "terbaik" untuk melakukan hal ini. Saya telah menemukan tiga metode ini yang mungkin bekerja paling baik di lingkungan yang berbeda:

  • Mungkin tidak berfungsi di beberapa lingkungan GCC:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • Mungkin tidak berfungsi di beberapa lingkungan 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)
    
  • Muncul untuk berfungsi di GCC dan 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);
    
  • Intel compiler memiliki opsi untuk menonaktifkan denormals secara default pada CPU Intel modern. Lebih detail di sini

  • Sakelar kompilator. -ffast-math, -msse atau -mfpmath=sse akan menonaktifkan denormal dan membuat beberapa hal lain lebih cepat, tetapi sayangnya juga melakukan banyak pendekatan lain yang mungkin melanggar kode Anda. Uji dengan hati-hati! Setara matematika cepat untuk kompiler Visual Studio adalah /fp:fast tapi saya belum bisa memastikan apakah ini juga menonaktifkan denormal.1


29
2018-02-26 12:15



Dalam gcc Anda dapat mengaktifkan FTZ dan DAZ dengan ini:

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

juga menggunakan switch gcc: -msse -mfpmath = sse

(kredit yang sesuai untuk Carl Hetherington [1])

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


19
2017-10-02 04:40



Komentar Dan Neely harus diperluas menjadi jawaban:

Itu bukan nol konstan 0.0f yang denormalized atau menyebabkan perlambatan, itu adalah nilai-nilai yang mendekati nol setiap iterasi loop. Ketika mereka mendekati dan mendekati nol, mereka membutuhkan lebih banyak ketepatan untuk mewakili dan mereka menjadi denormalized. Ini adalah y[i] nilai-nilai. (Mereka mendekati nol karena x[i]/z[i] kurang dari 1,0 untuk semua i.)

Perbedaan penting antara versi lambat dan cepat dari kode adalah pernyataannya y[i] = y[i] + 0.1f;. Segera setelah baris ini dieksekusi setiap iterasi loop, presisi ekstra dalam float hilang, dan denormalization diperlukan untuk menyatakan bahwa ketepatan tidak lagi diperlukan. Setelah itu, operasi floating point di y[i] tetap cepat karena mereka tidak denormalized.

Mengapa ketelitian ekstra hilang saat Anda menambahkan 0.1f? Karena angka floating point hanya memiliki banyak digit signifikan. Katakanlah Anda memiliki penyimpanan yang cukup untuk tiga digit signifikan, lalu 0.00001 = 1e-5, dan 0.00001 + 0.1 = 0.1, setidaknya untuk contoh format float ini, karena tidak memiliki ruang untuk menyimpan bit paling tidak signifikan di 0.10001.

Pendeknya, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; bukan tidak-op yang Anda pikirkan.

Mistik juga mengatakan ini: isi pelampung itu penting, bukan hanya kode perakitan.


0
2017-08-01 13:32