Почему изменение 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. (Я не тестировал другие компиляторы.)
- HttpWebRequest очень медленный!
- jQuery выбрать по classу VS выбрать по атрибуту
- Android: RunOnUiThread против AsyncTask
- Benchmark Linq2SQL, Subsonic2, Subsonic3 - Какие-нибудь другие идеи, чтобы сделать их быстрее?
- Является ли это очень важным для производительности?
- Инструменты для измерения затрат на связь MPI
- Зачем закрывать class?
- Секундомер против использования System.DateTime.Now для событий синхронизации
- Не пытайтесь ли вы блокировать блокировки, если исключения не выбрасываются?
- Является ли DateTime.Now лучшим способом измерения производительности функции?
- Как передавать значения на страницах ASP.net без использования сеанса
- Почему петли медленны в R?
- Каков самый быстрый способ обмена значениями в C?
Добро пожаловать в мир денормализованных плавающих точек ! Они могут нанести ущерб производительности!
Денормальные (или субнормальные) числа являются своего рода хаком, чтобы получить некоторые дополнительные значения, очень близкие к нулю из представления с плавающей запятой. Операции с денормализованной плавающей точкой могут быть в десятки и сотни раз медленнее, чем при нормализованной плавающей запятой. Это связано с тем, что многие процессоры не могут обрабатывать их напрямую и должны ловить их и разрешать с помощью микрокода.
Если вы распечатаете цифры после 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
преобразуется / сохраняется в регистр за пределами обеих петель. Таким образом, это не влияет на производительность.
Использование 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;
и используя вместо 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 можно найти в вопросе переполнения стека. Денормализованная плавающая точка в Objective-C? ,
Это связано с денормализованным использованием с плавающей запятой. Как избавиться от него и от штрафа за производительность? Просматривая Интернет для способов убийства денормальных чисел, кажется, что «лучшего» способа сделать это пока нет. Я нашел эти три метода, которые могут работать лучше всего в разных средах:
-
Возможно, не работает в некоторых средах GCC:
// Requires #include
fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV); -
Возможно, не работает в некоторых средах Visual Studio: 1
// Requires #include
_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
// Requires #include _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
В gcc вы можете включить FTZ и DAZ следующим образом:
#include #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])
Комментарий Дан Нили следует расширить в ответ:
Это не нулевая константа 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;
y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
это не то, что вы можете себе представить.
Мистик также сказал об этом : содержание поплавков имеет значение, а не только код сборки.