Как заставить 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.

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

57
задан 27 August 2019 в 15:54

4 ответа

Можно записать 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 non-NaN и неотрицательный. Мы все еще получаем ( Godbolt) ту же консервированную последовательность asm, которая проверяет на x>=0 и иначе звонит sqrtf для установки errno. , По-видимому, что расширение в сравнивать-и-переходить происходит после других оптимизационных проходов, , таким образом, оно не помогает для компилятора знать больше.

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

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

, Это - 100%-й сейф, если Вы не полагаетесь на математические функции никогда установка errno . Никто не хочет это, это - то, для чего распространение NaN и/или липкие флаги, которые записывают исключения FP маскированные. например, C99/C ++ 11 fenv доступ через [1 115] и затем функционирует как [1 139] fetestexcept() . Посмотрите пример в [1 140] feclearexcept , который показывает использование его для обнаружения деления на нуль.

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

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

Реальные аппаратные средства FPU действительно на самом деле имеют эти липкие флаги, которые остаются установленными, пока не очищено, например, x86 mxcsr состояние/регистр управления для математики SSE/AVX или аппаратные средства FPUs в другом ISAs. На аппаратных средствах, где FPU может обнаружить исключения, качество, реализация C++ будет поддерживать материал как [1 123]. И в противном случае затем математика - errno, вероятно, не работает также.

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

Это - большая ошибка смешать -fno-math-errno в с изменяющей значение оптимизацией как [1 127] или -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 с [1 130]. (Хотя та опция не требуется для этой оптимизации, это только errno - устанавливающий дерьмо, это - проблема здесь.).

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

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

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

46
ответ дан 1 November 2019 в 17:13

Передайте опцию -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 математических подпрограмм, это походит на хорошее решение.

9
ответ дан 1 November 2019 в 17:13

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

#include <immintrin.h>

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

(на godbolt)

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

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

5
ответ дан 1 November 2019 в 17:13

Приблизительно после недели, я спрошенный относительно вопроса на 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

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

2
ответ дан 1 November 2019 в 17:13

Другие вопросы по тегам:

Похожие вопросы: