Что такое инструкция, которая дает нерасширяемый FP min и max на x86?

Процитировать (спасибо автору за разработку и использование алгоритма!):

https://tavianator.com/fast-branchless-raybounding-box-intersections/

Поскольку современные наборы команд с плавающей запятой могут вычислять min и max без ветвей

Соответствующий код автора - это просто

dmnsn_min(double a, double b)
{
  return a < b ? a : b;
}

Я знаю, например, _mm_max_ps, но это векторная инструкция. Очевидно, что код, очевидно, предназначен для использования в скалярной форме.

Вопрос:

  • Что такое скалярная нераспределенная инструкция minmax на x86? Это последовательность инструкций?
  • Можно ли предположить, что он будет применяться, или как его назвать?
  • Имеет ли смысл беспокоиться о бесветренности min/max? Из того, что я понимаю, для raytracer и/или другого программного обеспечения, с учетом процедуры пересечения лучей, нет надежного шаблона для прогнозирования ветвления, поэтому имеет смысл устранить ветвь. Я прав об этом?
  • Самое главное, обсуждаемый алгоритм строится вокруг сравнения с (+/-) INFINITY. Является ли это надежной w.r.t(неизвестной) инструкцией, которую мы обсуждаем, и стандартом с плавающей запятой?

На всякий случай: я знаком с Использование минимальных и максимальных функций в С++, считаю, что это связано, но не совсем с моим вопросом.

Ответы

Ответ 1

Большинство векторных инструкций FP имеют скалярные эквиваленты. MINSS/MAXSS/MINSD/MAXSD - это то, что вы хотите. Они обрабатывают +/- Infinity так, как вы ожидали.

MINSS a,b точно реализует (a<b) ? a : b в соответствии с правилами IEEE, со всем, что подразумевает о знаках с нулевым значением, NaN и Infinities. Это означает, что компиляторы могут использовать их для std::min и std::max, потому что эти функции основаны на одном выражении.

Не пытайтесь использовать _mm_min_ss для скалярных поплавков; внутренняя версия доступна только с операндами __m128, а Intel intrinsics не предоставляет никакого способа получить скалярный поплавок в низкий элемент __m128 без обнуления высоких элементов или как-то делать дополнительную работу. Большинство компиляторов действительно будут генерировать бесполезные инструкции, чтобы сделать это, даже если конечный результат не зависит от чего-либо в верхних элементах. Нет ничего лучше __m256 _mm256_castps128_ps256 (__m128 a), чтобы просто поместить float в __m128 с мусором в верхних элементах. Я считаю это ошибкой дизайна.:/


Обратите внимание на их асимметричное поведение с помощью NaN: если операнды неупорядочены, dest = src (т.е. он принимает второй операнд, если один из операндов равен NaN). Это может быть полезно для условных обновлений SIMD, см. Ниже.

(a и b являются неупорядоченными, если любое из них - NaN. Это означает, что a<b, a==b и a>b все ложные. См. Брюс Доусон, серия статей о плавающей запятой для большого количества FP gotchas.)

Соответствующие функции _mm_min_ss/_mm_min_ps могут иметь или не иметь такого поведения, в зависимости от компилятора.

Я думаю, что intrinsics должны иметь такую ​​же семантику операнда-порядка, что и инструкции asm, но gcc обрабатывает операнды до _mm_min_ps как коммутативные, даже без -ffast-math в течение длительного времени, возвращаясь, по крайней мере, к gcc4 +0,4. gcc7, наконец, изменил его на соответствие icc и clang. Тем не менее, встроенный поисковый запрос Intel не упоминает о поведении функции, но OTOH в руководстве asm insn ref не упоминает о том, что внутреннее поведение не имеет поведения, когда оно перечисляет _mm_min_ss как внутреннее для MINSS.

Когда я googled на "_mm_min_ps" NaN, я нашел этот реальный код и некоторое другое обсуждение использования встроенного для обработки NaNs, так ясно многие люди ожидают, что внутреннее поведение будет вести себя как инструкция asm. (Это придумал какой-то код, который я пишу вчера, и я уже думал написать его как ответ на вопрос Q & A.)

Учитывая наличие этой многолетней ошибки gcc, переносимый код, который хочет воспользоваться обработкой MINPS NaN, должен принять меры предосторожности. Стандартная версия gcc на многих существующих дистрибутивах Linux будет неправильно компилировать ваш код, если он зависит от порядка операндов до _mm_min_ps. Поэтому вам, вероятно, понадобится #ifdef для обнаружения фактического gcc (не clang и т.д.) И альтернативы. Или просто сделайте это по-другому:/Возможно, с _mm_cmplt_ps и логическим AND/ANDNOT/OR.

Включение -ffast-math также делает коммутатор _mm_min_ps коммутативным для всех компиляторов.


Как обычно, компиляторы знают, как использовать набор команд для правильной реализации семантики C. MINSS и MAXSS быстрее, чем все, что вы могли бы сделать с веткой в ​​любом случае, так что просто напишите код, который может скомпилировать один из них.

Коммутативная ошибка _mm_min_ps применима только к внутреннему: gcc точно знает, как работает MINSS/MINPS, и использует их для правильной реализации строгой семантики FP (если вы не используете -ffast-math).

Обычно вам не нужно ничего делать, чтобы получить достойный скалярный код из компилятора. Если вы собираетесь тратить время на то, какие инструкции использует компилятор, вам, вероятно, следует начать с векторизации кода вручную, если компилятор этого не делает.

(Возможны редкие случаи, когда ветка лучше всего, если условие почти всегда идет в одну сторону, а латентность важнее пропускной способности. Задержка MINPS составляет ~ 3 цикла, но отлично предсказанная ветвь добавляет 0 циклов в цепочку зависимостей критического пути.)


В С++ используйте std::min и std::max, которые определены в терминах > или < и не имеют одинаковых требований к поведению NaN, которые выполняются fmin и fmax. Избегайте fmin и fmax, если вам не требуется их поведение NaN.

В C я думаю, что просто напишите свои собственные функции min и max (или макросы, если вы сделаете это безопасно).


C и asm в проводнике компилятора Godbolt

float minfloat(float a, float b) {
  return (a<b) ? a : b;
}
# any decent compiler (gcc, clang, icc), without any -ffast-math or anything:
    minss   xmm0, xmm1
    ret

// C++
float minfloat_std(float a, float b) { return std::min(a,b); }
  # This implementation of std::min uses (b<a) : b : a;
  # So it can only produce the result in the register that b was in
  # This isn't worse (when inlined), just opposite
    minss   xmm1, xmm0
    movaps  xmm0, xmm1
    ret


float minfloat_fmin(float a, float b) { return fminf(a, b); }

# clang inlines fmin; other compilers just tailcall it.
minfloat_fmin(float, float):
    movaps  xmm2, xmm0
    cmpunordss      xmm2, xmm2
    movaps  xmm3, xmm2
    andps   xmm3, xmm1
    minss   xmm1, xmm0
    andnps  xmm2, xmm1
    orps    xmm2, xmm3
    movaps  xmm0, xmm2
    ret
   # Obviously you don't want this if you don't need it.

Если вы хотите использовать _mm_min_ss/_mm_min_ps самостоятельно, напишите код, который позволяет компилятору сделать хороший asm даже без -ffast-math.

Если вы не ожидаете NaNs или хотите обрабатывать их специально, напишите такие вещи, как

lowest = _mm_min_ps(lowest, some_loop_variable);

поэтому регистр, содержащий lowest, может быть обновлен на месте (даже без AVX).


Воспользовавшись поведением MINPS NaN:

Скажите, что ваш скалярный код похож на

if(some condition)
    lowest = min(lowest, x);

Предположим, что условие может быть векторизовано с помощью CMPPS, поэтому у вас есть вектор элементов с битами, все установленные или все четкие. (Или, может быть, вы можете уйти с ANDPS/ORPS/XORPS на поплавках напрямую, если вы просто заботитесь о своем знаке и не заботитесь о отрицательном ноле. Это создает значение истины в бите знака с мусором в другом месте. BLENDVPS только выглядит на знаке, так что это может быть очень полезно. Или вы можете транслировать бит знака с помощью PSRAD xmm, 31.)

Прямым способом реализовать это будет сочетание x с +Inf на основе маски условия. Или сделайте newval = min(lowest, x); и добавьте newval в lowest. (либо BLENDVPS, либо AND/ANDNOT/OR).

Но фокус в том, что все-один бит - это NaN, а побитовое ИЛИ будет распространять его. Итак:

__m128 inverse_condition = _mm_cmplt_ps(foo, bar);
__m128 x = whatever;


x = _mm_or_ps(x, condition);   // turn elements into NaN where the mask is all-ones
lowest = _mm_min_ps(x, lowest);  // NaN elements in x mean no change in lowest
//  REQUIRES NON-COMMUTATIVE _mm_min_ps: no -ffast-math
//  AND DOESN'T WORK AT ALL WITH MOST GCC VERSIONS.

Итак, только с SSE2, и мы выполнили условные MINPS в двух дополнительных инструкциях (ORPS и MOVAPS, если разворот цикла не позволяет MOVAPS исчезнуть).

Альтернативой без SSE4.1 BLENDVPS является ANDPS/ANDNPS/ORPS для смешивания плюс дополнительные MOVAPS. ORPS более эффективен, чем BLENDVPS, в любом случае (это два раза на большинстве процессоров).

Ответ 2

Питер Кордес отвечает, здорово, я просто подумал, что я бы прыгнул с более короткими точными ответами:

  • Что такое скалярная нераспределенная инструкция minmax на x86? Это последовательность инструкций?

Я имел в виду minss/minsd. И даже другие архитектуры без таких инструкций должны иметь возможность делать это безветренно с условными ходами.

  • Можно ли предположить, что он будет применяться, или как его назвать?

gcc и clang будут оптимизировать (a < b) ? a : b до minss/minsd, поэтому я не буду беспокоиться об использовании встроенных функций. Нельзя говорить с другими компиляторами.

  • Имеет ли смысл беспокоиться о бесветренности min/max? Из того, что я понимаю, для raytracer и/или другого программного обеспечения, с учетом процедуры пересечения лучей, нет надежного шаблона для прогнозирования ветвления, поэтому имеет смысл устранить ветвь. Я прав об этом?

Индивидуальные тесты a < b в значительной степени непредсказуемы, поэтому очень важно избегать ветвления для них. Тесты, такие как if (ray.dir.x != 0.0), очень предсказуемы, поэтому избегать этих ветвей не так важно, но оно уменьшает размер кода и упрощает его векторизация. Наиболее важная часть - это, вероятно, удаление разделов.

  • Самое главное, обсуждаемый алгоритм строится вокруг сравнения с (+/-) INFINITY. Является ли это надежной w.r.t(неизвестной) инструкцией, которую мы обсуждаем, и стандартом с плавающей запятой?

Да, minss/minsd ведут себя точно как (a < b) ? a : b, включая их обработку бесконечностей и NaNs.

Кроме того, я написал отчет о последующих действиях к тому, на который вы ссылались, что более подробно рассказывает о NaN и min/max.