Плохая оптимизация 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 ;