Ответ 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, в любом случае (это два раза на большинстве процессоров).