Чому зміна 0,1f на 0 уповільнює продуктивність на 10 разів?


1527

Чому цей біт коду,

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

запустити більш ніж у 10 разів швидше, ніж наступний біт (ідентичний за винятком випадків, коли зазначено)?

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. Рівень оптимізації був -02з sse2підтримкою. Я не тестував інших компіляторів.


10
Як ви виміряли різницю? А які варіанти ви використовували під час компіляції?
Джеймс Канзе

158
Чому в цьому випадку компілятор просто не скидає +/- 0?!?
Майкл Дорган

127
@ Zyx2000 Компілятор ніде не є таким дурним. Дизасемблювання тривіального прикладу в LINQPad показує , що він випльовує один і той же код , чи використовуєте ви 0, 0f, 0dабо навіть (int)0в контексті , де doubleнеобхідно.
мільйозу

14
який рівень оптимізації?
Otto Allmendinger

Відповіді:


1616

Ласкаво просимо у світ денормалізованої плаваючої точки ! Вони можуть завдати шкоди продуктивності !!!

Денормальні (або субнормальні) числа є своєрідним хаком для отримання додаткових значень, близьких до нуля, із представлення плаваючої точки. Операції з денормалізованою плаваючою точкою можуть бути в десятки в сотні разів повільнішими, ніж на нормалізованій плаваючій точці. Це пояснюється тим, що багато процесорів не можуть безпосередньо з ними поводитися, і вони повинні вловлювати та вирішувати їх за допомогою мікрокоду.

Якщо ви роздрукуєте цифри після 10 000 ітерацій, ви побачите, що вони перетворилися на різні значення залежно від того , використовується 0або 0.1використовується.

Ось тестовий код, складений на 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;
}

Вихід:

#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

Зверніть увагу, як у другому циклі цифри дуже близькі до нуля.

Денормалізовані числа, як правило, рідкісні, і тому більшість процесорів не намагаються впоратися з ними ефективно.


Щоб продемонструвати, що це має все відношення до денормалізованих чисел, якщо ми зведемо денормали до нуля , додавши це до початку коду:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Тоді версія з 0уже не на 10 разів повільніше і фактично стає швидшою. (Для цього потрібно скомпілювати код із включеним SSE.)

Це означає, що замість того, щоб використовувати ці дивні більш низькі точності майже нульових значень, ми просто округлимо до нуля.

Часи синхронізації: Core i7 920 при 3,5 ГГц:

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

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

Зрештою, це насправді не має нічого спільного з тим, чи це ціле число, або плаваюча точка. 0Або 0.1fперетворюється / зберігається в регістрі зовні обох петель. Тож це не впливає на продуктивність.


100
Я все ще вважаю трохи дивним, що компілятор за замовчуванням не оптимізовано компілятором "+ 0". Це сталося б, якби він поставив "+ 0,0f"?
s73v3r

51
@ s73v3r Це дуже гарне запитання. Тепер, коли я дивлюся на збірку, навіть + 0.0fне оптимізується. Якби я мав здогадуватися, можливо, це було + 0.0fб побічними ефектами, якщо y[i]трапилося б сигналізація NaNчи щось таке ... я можу помилитися.
Містичний

14
Парні все ще матимуть одну проблему у багатьох випадках, лише з різною числовою величиною. "Зниміть на нуль" добре для аудіо-програм (та інших, де ви можете дозволити собі втратити 1e-38 тут і там), але я вважаю, що це не стосується x87. Без FTZ, звичайним виправленням для аудіоприкладних програм є введення дуже низької амплітуди (не чутний) постійного або квадратного хвильового сигналу до номерів тремтіння подалі від денормальності.
Рассел Борогов

16
@Isaac, оскільки коли y [i] значно менше 0,1 додавання, це призводить до втрати точності, оскільки найзначніша цифра числа стає вище.
Дан вигадує Firelight

167
@ s73v3r: + 0.f не можна оптимізувати, оскільки плаваюча точка має мінус 0, а результат додавання + 0.f до -0f дорівнює + 0.f. Отже додавання 0.f не є операцією з ідентифікацією, і її неможливо оптимізувати.
Eric Postpischil

415

Використання gccта застосування диференціалу до створеної збірки дає лише таку різницю:

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

Той cvtsi2ssqдійсно в 10 разів повільніше.

Мабуть, floatверсія використовує реєстр XMM, завантажений з пам'яті, тоді як intверсія перетворює реальне intзначення 0 на floatвикористання cvtsi2ssqінструкції, забираючи багато часу. Перехід -O3на gcc не допомагає. (версія gcc 4.2.1.)

(Використання doubleзамість floatне має значення, за винятком того, що воно перетворює cvtsi2ssqна а cvtsi2sdq.)

Оновлення

Деякі додаткові тести показують, що це не обов'язково cvtsi2ssqінструкція. Після усунення (використання a int ai=0;float a=ai;і використання aзамість 0) різниця швидкостей залишається. Тож @Mysticial має рацію, денормалізовані поплавці мають значення. Це можна зрозуміти, перевіривши значення між 0та 0.1f. Точка повороту у наведеному вище коді приблизно на 0.00000000000000000000000000000001, коли петлі раптово тривають у 10 разів.

Оновлення << 1

Невелика візуалізація цього цікавого явища:

  • Колонка 1: поплавок, розділений на 2 для кожної ітерації
  • Стовпець 2: двійкове зображення цього поплавця
  • Колонка 3: час, необхідний для підсумовування цього плавця 1e7 разів

Ви чітко бачите, як показник (останні 9 біт) змінюється на найнижче значення, коли починається денормалізація. У цей момент просте додавання стає в 20 разів повільніше.

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 можна знайти в питанні про переповнення стека Денормалізована плаваюча точка в Objective-C? .


27
-Os не виправити це, але -ffast-mathробить. (Я використовую це весь час, IMO, кутові випадки, коли це спричиняє проблеми з точністю, все одно не повинні з'являтися в правильно розробленій програмі.)
Продовжившись

На будь-якому рівні позитивної оптимізації з gcc-4.6 не відбувається перетворення.
Джед

@leftaroundabout: компіляція виконуваного файлу (не бібліотеки) із -ffast-mathпосиланнями додаткового коду запуску, який встановлює FTZ (флеш до нуля) та DAZ (денормальний нуль) у MXCSR, тому процесору ніколи не потрібно приймати повільну мікрокодування для denormals.
Пітер Кордес

34

Це пов’язано з денормалізованим використанням з плаваючою комою. Як позбутися і від нього, і від виконання покарання? Переглянувши Інтернет для способів вбивства денормальних чисел, здається, ще немає «найкращого» способу зробити це. Я знайшов ці три методи, які можуть найкраще працювати в різних середовищах:

  • Може не працювати в деяких середовищах GCC:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
  • Може не працювати в деяких середовищах 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)
  • Здається, що працює як у GCC, так і у 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 має можливість вимкнення деннормалів за замовчуванням на сучасних процесорах Intel. Детальніше тут

  • Вимикачі компілятора. -ffast-math, -msseабо -mfpmath=sseвідключить денноріуми і зробить кілька інших речей швидше, але, на жаль, також зробить багато інших наближень, які можуть порушити ваш код. Тестуйте уважно! Еквівалент швидкої математики для компілятора Visual Studio є, /fp:fastале я не зміг підтвердити, чи це також вимикає денноріуми. 1


1
Це звучить як гідна відповідь на інше, але пов'язане з цим питання (як я можу запобігти чисельним обчисленням давати денормальні результати?) Хоча це питання не дає відповіді.
Ben Voigt

Під час запуску .exe Windows X64 передає налаштування несподіваного підтоку, тоді як 32-розрядні та Linux Linux не роблять. У Linux Linux gcc -ffast-math повинен встановлювати різкий підтік (але я думаю, що не в Windows). Передбачається, що компілятори Intel ініціалізуються в main (), щоб ці відмінності в ОС не проходили, але мене покусали, і потрібно встановити це явно в програмі. Процесорні процесори Intel, що починаються з Sandy Bridge, повинні ефективно обробляти субнормали, що виникають при складанні / відніманні (але не діленні / множенні), тому є можливість використовувати поступовий підтік.
tim18

1
Microsoft / fp: швидко (не за замовчуванням) не робить жодної агресивної речі, властивої gcc -ffast-math або ICL (за замовчуванням) / fp: fast. Це більше схоже на ICL / fp: source. Тому ви повинні встановити / fp: (а в деяких випадках і в режимі підпорядкування) явно, якщо ви хочете порівняти ці компілятори.
tim18

18

У gcc ви можете ввімкнути FTZ та DAZ за допомогою цього:

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

також використовуйте gcc-комутатори: -msse -mfpmath = sse

(відповідні кредити Карлу Гетерінгтону [1])

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


Також дивіться fesetround()з fenv.h(визначено для C99) іншого, більш портативного способу округлення ( linux.die.net/man/3/fesetround ) (але це вплине на всі операції ПП, а не лише на субнормальних )
німецький Garcia

Ви впевнені, що вам потрібні 1 << 15 та 1 << 11 для FTZ? Я бачив лише 1 << 15, процитований в інших місцях ...
рис.

@fig: 1 << 11 призначений для маски Underflow. Більше інформації тут: softpixel.com/~cwright/programming/simd/sse.php
Німецький Гарсія

@GermanGarcia це не відповідає на питання ОП; питання було "Чому цей біт коду працює в 10 разів швидше, ніж ..." - вам слід спробувати відповісти на це, перш ніж надати це рішення, або надати це в коментарі.

9

Коментар Дена Нілі слід розширити на відповідь:

Не нульова константа 0.0fє денормалізованою або спричинює уповільнення, це значення, що наближаються до нуля кожної ітерації циклу. Коли вони наближаються і наближаються до нуля, їм потрібно більше точності представляти, і вони стають денормалізованими. Це y[i]значення. (Вони наближаються до нуля, оскільки x[i]/z[i]менше 1,0 для всіх i.)

Вирішальною різницею повільної та швидкої версій коду є твердження y[i] = y[i] + 0.1f;. Як тільки ця лінія виконує кожну ітерацію циклу, додаткова точність у поплавці втрачається, і денормалізація, необхідна для представлення цієї точності, вже не потрібна. Після цього операції з плаваючою комою y[i]залишаються швидкими, оскільки вони не денормалізовані.

Чому при додаванні втрачається додаткова точність 0.1f? Тому що числа з плаваючою комою мають лише стільки значущих цифр. Скажімо, у вас є достатньо місця для зберігання трьох значущих цифр 0.00001 = 1e-5і 0.00001 + 0.1 = 0.1, принаймні, для цього прикладу плаваючого формату, оскільки в ньому немає місця для зберігання найменш значущого біта 0.10001.

Коротше кажучи, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;чи не той варіант, який, напевно, можна подумати.

Містичний також сказав це : зміст плаваючих елементів має значення, а не лише код складання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.