Почему изменение 0.1f to 0 замедляет производительность на 10x?

Почему этот бит кода,

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. (Я не тестировал другие компиляторы.)

Ответ 1

Добро пожаловать в мир denormalized floating-point! Они могут нанести ущерб производительности!!! p >

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

Если вы распечатываете номера после 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 перестает быть на 10x медленнее и на самом деле становится быстрее. (Это требует, чтобы код был скомпилирован с включенным 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 преобразуется/сохраняется в регистре вне обеих петель. Таким образом, это не влияет на производительность.

Ответ 2

Использование gcc и применение diff к сгенерированной сборке дает только эту разницу:

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. После устранения (используя int ai=0;float a=ai; и используя a вместо 0), разница в скорости остается. Итак, @Mysticial прав, денормализованные поплавки имеют значение. Это можно увидеть путем тестирования значений между 0 и 0.1f. Точка поворота в приведенном выше коде приблизительно равна 0.00000000000000000000000000000001, когда петли внезапно проходят в 10 раз.

Обновление < 1

Небольшая визуализация этого интересного явления:

  • Столбец 1: поплавок, разделенный на 2 для каждой итерации
  • Столбец 2: двоичное представление этого поплавка
  • Столбец 3: время, затраченное на суммирование этого поплавка 1 раз в 7 раз

Вы можете четко видеть, что показатель экспоненты (последние 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 можно найти в Stack Вопрос переполнения Денормализованная плавающая запятая в Objective-C?.

Ответ 3

Это связано с денормализованным использованием с плавающей запятой. Как избавиться от него и от штрафа за производительность? Просматривая Интернет для способов убийства денормальных чисел, кажется, что "лучшего" способа сделать это пока нет. Я нашел эти три метода, которые могут работать лучше всего в разных средах:

  • Возможно, не работает в некоторых средах 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

Ответ 4

В 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

Ответ 5

Комментарий Дана Нили должен быть расширен в ответ:

Это не нулевая константа 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 бита в 0.10001.

Короче говоря, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; разве вы не думаете, что это не так?

Мистик сказал и это: имеет значение содержание float, а не только код сборки.