Почему этот бит кода,
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
включенным. Я не тестировал с другими компиляторами.
0
,0f
,0d
или даже(int)0
в контексте , гдеdouble
необходимо.Ответы:
Добро пожаловать в мир денормализованных чисел с плавающей точкой ! Они могут нанести ущерб производительности !!!
Денормальные (или субнормальные) числа являются своего рода хаком, чтобы получить некоторые дополнительные значения, очень близкие к нулю, из представления с плавающей запятой. Операции с денормализованной плавающей точкой могут быть в десятки и сотни раз медленнее, чем с нормализованной плавающей точкой. Это потому, что многие процессоры не могут обрабатывать их напрямую и должны перехватывать и разрешать их с помощью микрокода.
Если вы распечатаете числа после 10 000 итераций, вы увидите, что они сходятся к разным значениям в зависимости от того, используется
0
или0.1
используется.Вот тестовый код, скомпилированный на x64:
Вывод:
Обратите внимание, что во втором запуске числа очень близки к нулю.
Денормализованные числа, как правило, встречаются редко, и поэтому большинство процессоров не пытаются эффективно с ними справиться.
Чтобы продемонстрировать, что это имеет отношение к денормализованным числам, если мы сбрасываем денормалы в ноль , добавляя это в начало кода:
Тогда версия с
0
больше не будет в 10 раз медленнее и фактически станет быстрее. (Для этого необходимо, чтобы код был скомпилирован с включенным SSE.)Это означает, что вместо того, чтобы использовать эти странные почти нулевые значения с более низкой точностью, мы вместо этого просто округляем до нуля.
Времена: Core i7 920 @ 3,5 ГГц:
В конце концов, это не имеет ничего общего с целым числом или с плавающей точкой.
0
Или0.1f
преобразуется / хранится в регистре снаружи обеих петель. Так что это не влияет на производительность.источник
+ 0.0f
не оптимизируется. Если бы мне пришлось угадывать, это могло+ 0.0f
бы иметь побочные эффекты, если быy[i]
это было сигналомNaN
или чем-то ... Я могу ошибаться.Использование
gcc
и применение diff к сгенерированной сборке дает только эту разницу:Тот,
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
Небольшая визуализация этого интересного явления:
Вы можете ясно видеть, как показатель степени (последние 9 бит) меняется на самое низкое значение, когда начинается денормализация. В этот момент простое добавление становится в 20 раз медленнее.
Эквивалентное обсуждение ARM можно найти в вопросе переполнения стека Денормализованная плавающая точка в Objective-C? ,
источник
-O
Это не исправить, но-ffast-math
делает. (Я использую это все время, IMO в тех случаях, когда это вызывает проблемы с точностью, в любом случае не-ffast-math
ссылками некоторого дополнительного кода запуска, который устанавливает FTZ (сбрасывать на ноль) и DAZ (ненормированные равны нулю) в MXCSR, поэтому ЦП никогда не приходится принимать медленную помощь микрокода для денормализаций.Это из-за денормализованного использования с плавающей точкой. Как избавиться от этого и от потери производительности? Поискав в Интернете способы уничтожения ненормальных чисел, кажется, что пока нет «лучшего» способа сделать это. Я нашел эти три метода, которые могут лучше всего работать в разных средах:
Может не работать в некоторых средах GCC:
Может не работать в некоторых средах Visual Studio: 1
Появляется для работы в GCC и Visual Studio:
Компилятор Intel имеет опции для отключения денормальных значений по умолчанию на современных процессорах Intel. Подробнее здесь
Переключатели компилятора.
-ffast-math
,-msse
Или-mfpmath=sse
отключит денормализованные числа и сделать несколько других вещей быстрее, но , к сожалению , также сделать много других приближений , которые могут нарушить ваш код. Проверьте внимательно! Эквивалентом быстрой математики для компилятора Visual Studio является,/fp:fast
но я не смог подтвердить, отключает ли это также денормали. 1источник
В gcc вы можете включить FTZ и DAZ с помощью этого:
также используйте ключи gcc: -msse -mfpmath = sse
(соответствующие кредиты Карлу Хетерингтону [1])
[1] http://carlh.net/plugins/denormals.php
источник
fesetround()
отfenv.h
(определяются для C99) для другого, более переносимого способа округления ( linux.die.net/man/3/fesetround ) (но это будет влиять на все операции FP, а не только subnormals )Комментарий Дана Нили должен быть расширен в ответ:
Это не нулевая константа,
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;
разве вы не думаете, что это не так?Мистик сказал и это : важно содержимое, а не только ассемблерный код.
источник