Плохая оптимизация std:: fabs()?
Недавно я работал с приложением, имеющим код, похожий на:
for (auto x = 0; x < width - 1 - left; ++x)
{
// store / reset points
temp = hPoint = 0;
for(int channel = 0; channel < audioData.size(); channel++)
{
if (peakmode) /* fir rms of window size */
{
for (int z = 0; z < sizeFactor; z++)
{
temp += audioData[channel][x * sizeFactor + z + offset];
}
hPoint += temp / sizeFactor;
}
else /* highest sample in window */
{
for (int z = 0; z < sizeFactor; z++)
{
temp = audioData[channel][x * sizeFactor + z + offset];
if (std::fabs(temp) > std::fabs(hPoint))
hPoint = temp;
}
}
.. some other code
}
... some more code
}
Это внутри графического цикла рендеринга, называемого примерно 50-100 раз/сек с буферами до 192 кГц в нескольких каналах. Так что много данных, проходящих через самые внутренние циклы, и профилирование показали, что это горячая точка.
Мне пришло в голову, что можно было поместить float в целое число и стереть бит знака и отбросить его, используя только временные. Это выглядело примерно так:
if ((const float &&)(*((int *)&temp) & ~0x80000000) > (const float &&)(*((int *)&hPoint) & ~0x80000000))
hPoint = temp;
Это дало 12-кратное сокращение времени рендеринга, при этом все же выдавая тот же действительный результат. Обратите внимание, что все в аудиодатах предварительно очищается, чтобы не включать nans/infs/denormals и иметь только диапазон [-1, 1].
Есть ли какие-либо угловые случаи, когда эта оптимизация даст неправильные результаты - или, почему стандартная функция библиотеки не реализована так? Я предполагаю, что это связано с обработкой ненормальных значений?
e: макет модели с плавающей точкой соответствует ieee, а sizeof (float) == sizeof (int) == 4
Ответы
Ответ 1
Ну, вы настроили режим с плавающей запятой на соответствие IEEE. Как правило, с такими переключателями, как --fast-math
, компилятор может игнорировать угловые случаи IEEE, такие как NaN, INF и denormals. Если компилятор также использует встроенные функции, он, вероятно, может испускать один и тот же код.
Кстати, если вы собираетесь принять IEEE-формат, нет необходимости, чтобы откат возвращался к float до сравнения. Формат IEEE отличен: для all положительные конечные значения a<b
тогда и только тогда, когда reinterpret_cast<int_type>(a) < reinterpret_cast<int_type>(b)
Ответ 2
Мне пришло в голову, что можно было поместить float в целое число и стереть бит знака и отбросить его, используя только временные.
Нет, вы не можете, потому что это нарушает правило строгое выравнивание.
Есть ли какие-либо угловые случаи, когда эта оптимизация даст неправильные результаты
Технически этот код приводит к поведению undefined, поэтому он всегда дает неверные "результаты". Не в том смысле, что результат абсолютного значения всегда будет неожиданным или неправильным, но в том смысле, что вы не можете рассуждать о том, что делает программа, если она имеет поведение undefined.
или, почему стандартная функция библиотеки не реализована так?
Ваше подозрение оправдано, обработка денормалов и других исключительных значений сложна, функция stdlib также должна учитывать их, а другая причина по-прежнему является undefined.
Одно (не) решение, если вы заботитесь о производительности:
Вместо кастингов и указателей вы можете использовать объединение. К сожалению, это работает только на C, а не на С++. Это не приведет к UB, но он все еще не переносится (хотя, скорее всего, он будет работать с большинством, если не все, платформами с IEEE -754).
union {
float f;
unsigned u;
} pun = { .f = -3.14 };
pun.u &= ~0x80000000;
printf("abs(-pi) = %f\n", pun.f);
Но, предоставленный, это может или не может быть быстрее, чем вызов fabs()
. Уверена только одна вещь: это будет не всегда правильно.
Ответ 3
Ожидалось, что fabs()
будет реализован на аппаратном уровне. В 1980 году для него было 8087 инструкций. Вы не собираетесь бить аппаратное обеспечение.
Ответ 4
Как реализует стандартная библиотечная функция... зависит от реализации. Таким образом, вы можете найти различную реализацию стандартной библиотеки с разной производительностью.
Я предполагаю, что у вас могут быть проблемы на платформах, где int
не 32 бит. Лучше использовать int32_t (cstdint > )
Насколько я знаю, был ли ранее std:: abs? Или оптимизация, которую вы наблюдаете, в основном связана с подавлением вызова функции?
Ответ 5
Некоторые замечания о том, как рефакторинг может повысить производительность:
-
как уже упоминалось, x * sizeFactor + offset
может быть учтен из внутренних циклов
-
peakmode
на самом деле является переключателем, изменяющим поведение функции - выполняет две функции, а не тестирует промежуточную петлю коммутатора. Это имеет 2 преимущества:
- проще поддерживать
- меньше локальных переменных и кодовых путей, чтобы мешать оптимизации.
-
Разделение temp
на sizeFactor
может быть отложено до тех пор, пока не закончится цикл channel
в версии peakmode
.
-
abs(hPoint)
может быть предварительно вычислено при обновлении hPoint
-
если audioData
- вектор векторов, вы можете получить некоторое преимущество в производительности, взяв ссылку на audioData[channel]
в начале тела цикла channel
, уменьшив индексирование массива в z
цикл до одного измерения.
-
наконец, примените любые конкретные оптимизации для расчета fabs
, который вы считаете нужным. Все, что вы здесь делаете, будет вредить переносимости, поэтому это последнее средство.
Ответ 6
В VS2008 с помощью следующего для отслеживания абсолютного значения hpoint
и hIsNeg
, чтобы запомнить, является ли оно положительным или отрицательным, примерно в два раза быстрее, чем при использовании fabs()
:
int hIsNeg=0 ;
...
//Inside loop, replacing
// if (std::fabs(temp) > std::fabs(hPoint))
// hPoint = temp;
if( temp < 0 )
{
if( -temp > hpoint )
{
hpoint = -temp ;
hIsNeg = 1 ;
}
}
else
{
if( temp > hpoint )
{
hpoint = temp ;
hIsNeg = 0 ;
}
}
...
//After loop
if( hIsNeg )
hpoint = -hpoint ;