Как заставить GCC предположить, что выражение с плавающей точкой неотрицательно?

Есть случаи, когда вы знаете, что определенное выражение с плавающей точкой всегда будет неотрицательным. Например, при вычислении длины вектора выполняется sqrt(a[0]*a[0] + ... + a[N-1]*a[N-1]) (примечание: мне известно о std::hypot, это не относится к вопросу), и выражение под квадратным корнем явно неотрицательно. Однако GCC выводит следующую сборку для sqrt(x*x):

        mulss   xmm0, xmm0
        pxor    xmm1, xmm1
        ucomiss xmm1, xmm0
        ja      .L10
        sqrtss  xmm0, xmm0
        ret
.L10:
        jmp     sqrtf

То есть он сравнивает результат x*x с нулем, а если результат неотрицательный, он выполняет инструкцию sqrtss, в противном случае он вызывает sqrtf.

Итак, мой вопрос: как я могу заставить GCC предположить, что x*x всегда неотрицателен, так что он пропускает сравнение и вызов sqrtf, не записывая встроенную сборку?

Я хочу подчеркнуть, что меня интересует локальное решение, а не такие вещи, как -ffast-math, -fno-math-errno или -ffinite-math-only (хотя они действительно решают проблему, благодаря ks1322, Гарольду и Эрику Постпишилу в комментарии).

Кроме того, "заставить GCC предполагать, что x*x неотрицателен", следует интерпретировать как assert(x*x >= 0.f), поэтому это также исключает случай, когда x*x является NaN.

Я в порядке с решениями для компиляторов, платформ, процессоров и т.д.

Ответы

Ответ 1

assert(x*x >= 0.f) можно написать как обещание во время компиляции вместо проверки во время выполнения, как указано ниже в GNU C:

#include <cmath>

float test1 (float x)
{
    float tmp = x*x;
    if (!(tmp >= 0.0f)) 
        __builtin_unreachable();    
    return std::sqrt(tmp);
}

(связанный: Какие оптимизации обеспечивает __builtin_unreachable? Вы также можете заключить if(!x)__builtin_unreachable() в макрос и назвать его promise() или как-то еще.)

Но gcc не знает, как воспользоваться этим обещанием, что tmp не является NaN и неотрицательным. Мы по-прежнему получаем (Godbolt) ту же последовательность готовых asm, которая проверяет x>=0 и в противном случае вызывает sqrtf для установки errno. Предположительно, что расширение в сравнение и ветвление происходит после других этапов оптимизации,, поэтому компилятору не поможет узнать больше.

Это пропущенная оптимизация в логике, которая умозрительно указывает на sqrt, когда -fmath-errno включен (к сожалению, включен по умолчанию).

Вместо этого вы хотите -fno-math-errno, который безопасен во всем мире

Это на 100% безопасно, если вы не полагаетесь на математические функции, которые когда-либо устанавливали errno. Никто не хочет этого, для чего предназначены распространение NaN и/или липкие флаги, которые записывают скрытые исключения FP. например C99/C++ 11 fenv доступ через #pragma STDC FENV_ACCESS ON и затем работает как fetestexcept(). См. пример в feclearexcept, где показано его использование для обнаружения деления на ноль.

Среда FP является частью контекста потока, в то время как errno является глобальным.

Поддержка этой устаревшей несостоятельности не является бесплатной; Вы должны просто отключить его, если у вас нет старого кода, который был написан для его использования. Не используйте его в новом коде: используйте fenv. В идеале поддержка -fmath-errno была бы настолько дешевой, насколько это возможно, но редкость того, кто на самом деле использует __builtin_unreachable() или другие вещи, чтобы исключить ввод NaN, по-видимому, не стоила времени для разработчиков на реализацию оптимизации. Тем не менее, вы можете сообщить об ошибке пропущенной оптимизации, если хотите.

Реальное аппаратное обеспечение FPU на самом деле имеет эти липкие флаги, которые остаются установленными до сброса, например, x86 mxcsr регистр состояния/управления для математики SSE/AVX или аппаратных FPU в других ISA. На оборудовании, где FPU может обнаруживать исключения, качественная реализация C++ будет поддерживать такие вещи, как fetestexcept(). А если нет, то math- errno, вероятно, тоже не работает.

errno для математики был старым устаревшим проектом, с которым C/C++ по-прежнему придерживался по умолчанию, и теперь широко считается плохой идеей. Это усложняет компиляторам эффективную интеграцию математических функций. Или, может быть, мы не настолько застряли с этим, как я думал: Почему errno не установлен в EDOM, даже если sqrt снимает спор о домене? объясняет, что установка errno в математических функциях необязательна в ISO C11, и реализация может указывать, делают ли они это или нет. Предположительно и в C++.

Было бы большой ошибкой смешивать -fno-math-errno с оптимизациями, меняющими значение, такими как -ffast-math или -ffinite-math-only. Вам настоятельно рекомендуется включить его глобально или, по крайней мере, для всего файла, содержащего эту функцию.

float test2 (float x)
{
    return std::sqrt(x*x);
}
# g++ -fno-math-errno -std=gnu++17 -O3
test2(float):   # and test1 is the same
        mulss   xmm0, xmm0
        sqrtss  xmm0, xmm0
        ret

Вы также можете использовать -fno-trapping-math, если вы никогда не собираетесь снимать маску с каких-либо исключений FP с помощью feenableexcept(). (Хотя эта опция не требуется для этой оптимизации, проблема только в errno -setting.)

-fno-trapping-math не предполагает отсутствие NaN или чего-либо еще, а только то, что исключения FP, такие как Invalid или Inexact, никогда не будут вызывать обработчик сигнала вместо создания NaN или округленного результата. -ftrapping-math является значением по умолчанию, но он сломан и "никогда не работал", по словам разработчика GCC Марка Глисса. (Даже если он включен, GCC выполняет некоторые оптимизации, которые могут изменить количество исключений, которые будут повышены с нуля до ненулевого значения или наоборот. И это блокирует некоторые безопасные оптимизации). Но, к сожалению, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54192 (отключить по умолчанию) все еще открыт.

Если вы на самом деле когда-либо снимали исключения, возможно, было бы лучше иметь -ftrapping-math, но, опять же, очень редко вы захотите этого вместо простой проверки флагов после некоторых математических операций или проверки на NaN. И это на самом деле не сохраняет точную семантику исключений в любом случае.

См. SIMD для операции с пороговым значением с плавающей запятой для случая, когда -fno-trapping-math неправильно блокирует безопасную оптимизацию. (Даже после подъема потенциально ловушечной операции, так что C делает это безоговорочно, gcc создает не векторизованный asm, который делает это условно! Таким образом, он не только блокирует векторизацию, но и меняет семантику исключений по сравнению с абстрактной машиной C.)

Ответ 2

Передайте опцию -fno-math-errno в gcc. Это устраняет проблему, не делая ваш код непереносимым и не покидая сферу ISO/IEC 9899: 2011 (C11).

Эта опция не пытается установить errno при сбое функции математической библиотеки:

       -fno-math-errno
           Do not set "errno" after calling math functions that are executed
           with a single instruction, e.g., "sqrt".  A program that relies on
           IEEE exceptions for math error handling may want to use this flag
           for speed while maintaining IEEE arithmetic compatibility.

           This option is not turned on by any -O option since it can result
           in incorrect output for programs that depend on an exact
           implementation of IEEE or ISO rules/specifications for math
           functions. It may, however, yield faster code for programs that do
           not require the guarantees of these specifications.

           The default is -fmath-errno.

           On Darwin systems, the math library never sets "errno".  There is
           therefore no reason for the compiler to consider the possibility
           that it might, and -fno-math-errno is the default.

Учитывая, что вас не особо интересует настройка математических процедур errno, это кажется хорошим решением.

Ответ 3

Без каких-либо глобальных опций, вот (с минимальными издержками, но не бесплатно) способ получить квадратный корень без ветки:

#include <immintrin.h>

float test(float x)
{
    return _mm_cvtss_f32(_mm_sqrt_ss(_mm_set1_ps(x * x)));
}

(на кресте)

Как обычно, Clang умен в своих шаффлах. GCC и MSVC отстают в этой области, и им не удается избежать трансляции. MSVC также делает некоторые загадочные шаги..

Есть и другие способы превратить поплавок в __m128, например _mm_set_ss. Для Clang это не имеет значения, для GCC, который делает код немного больше и хуже (включая movss reg, reg, который считается для Intel случайным образом, поэтому он даже не экономит на случайном порядке).

Ответ 4

Примерно через неделю я спросил об этом в GCC Bugzilla & они предоставили решение, наиболее близкое к тому, что я имел в виду

float test (float x)
{
    float y = x*x;
    if (std::isless(y, 0.f))
        __builtin_unreachable();
    return std::sqrt(y);
}

который компилирует в следующую сборку:

test(float):
    mulss   xmm0, xmm0
    sqrtss  xmm0, xmm0
    ret

Я все еще не совсем уверен, что именно здесь происходит, хотя.