Минимум 4 СП значения в м128 микроконтроллеров

предположим, что есть __m128 переменная, содержащая 4 значения SP, и вы хотите минимальную, есть ли какая-либо внутренняя функция или что-либо другое, кроме наивного линейного сравнения между значениями?

право знать мое решение следующее (предположим, что на входе __m128 переменная x):

x = _mm_min_ps(x, (__m128)_mm_srli_si128((__m128i)x, 4));
min = _mm_min_ss(x, (__m128)_mm_srli_si128((__m128i)x, 8))[0];

что довольно ужасно, но он работает (кстати, есть ли что-нибудь вроде _mm_srli_si128 но __m128 тип?)

2 ответов


нет одной инструкции / внутренней, но вы можете сделать это с двумя перетасовками и двумя минутами:

__m128 _mm_hmin_ps(__m128 v)
{
    v = _mm_min_ps(v, _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 1, 0, 3)));
    v = _mm_min_ps(v, _mm_shuffle_ps(v, v, _MM_SHUFFLE(1, 0, 3, 2)));
    return v;
}

выходной вектор будет содержать минимум всех элементов входного вектора, повсюду в выходной вектор.


ответ Павла R велик! (@Paul R-Если Вы читаете это спасибо!) Я просто хотел попытаться объяснить, как это на самом деле работает для любого нового SSE, такого как я. Конечно Я могу ошибаться где-то, поэтому любые поправки приветствуются!

как _mm_shuffle_ps работы?

прежде всего, регистры SSE имеют индексы, которые идут в обратном направлении к тому, что вы могли бы ожидать, например:

[6, 9, 8, 5] // values
 3  2  1  0  // indexes

этот порядок индексирования делает векторными левыми сдвигами перемещение данных с низкого на высокие индексы, как и левый сдвиг битов в целое число. Самый важный элемент находится слева.


_mm_shuffle_ps можно смешивать содержимое двух регистров:

// __m128 a : (a3, a2, a1, a0)
// __m128 b : (b3, b2, b1, b0)
__m128 two_from_a_and_two_from_b = _mm_shuffle_ps(b, a, _MM_SHUFFLE(3, 2,   1, 0));
//                                                                  ^  ^    ^  ^ 
//                                            indexes into second operand    indexes into first operand
// two_from_a_and_two_from_b : (a3, a2, b1, b0)

здесь мы хотим только перетасовать значения одного регистра, а не двух. Мы можем сделать это, передав v как оба параметра, например (вы можете увидеть это в функции Paul R):

// __m128 v : (v3, v2, v1, v0)
__m128 v_rotated_left_by_1 = _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 1, 0, 3));
// v_rotated_left_by_1 : (v2, v1, v0, v3) // i.e. move all elements left by 1 with wraparound

я собираюсь обернуть его в макрос для удобочитаемости, хотя:

#define mm_shuffle_one(v, pattern)  _mm_shuffle_ps(v, v, pattern)

(It не может быть функцией, потому что до _mm_shuffle_ps должно быть постоянным во время компиляции.)

вот немного измененная версия фактической функции – я добавил промежуточные имена для удобочитаемости, так как компилятор оптимизирует их в любом случае:

inline __m128 _mm_hmin_ps(__m128 v){
    __m128  v_rotated_left_by_1 = mm_shuffle_one(v,  _MM_SHUFFLE(2, 1, 0, 3));
    __m128 v2 = _mm_min_ps(v,   v_rotated_left_by_1);

    __m128 v2_rotated_left_by_2 = mm_shuffle_one(v2, _MM_SHUFFLE(1, 0, 3, 2));
    __m128 v3 = _mm_min_ps(v2, v2_rotated_left_by_2);

    return v3;
}

почему перемешивание элементов так, как мы? И как мы находим наименьший из четырех элементов только с двумя min операции?

у меня были некоторые проблемы с тем, как вы можете min 4 поплавки с двумя векторизованными min операции, но я понял это, когда я вручную, следует, что значения minбудем вместе, шаг за шагом. (Хотя это, вероятно, более интересно сделать это самостоятельно, чем читать его)

скажем, у нас есть v:

[7,6,9,5] v

во-первых, мы min значения v и v_rotated_left_by_1:

[7,6,9,5] v
 3 2 1 0  // (just the indices of the elements)
[6,9,5,7] v_rotated_left_by_1
 2 1 0 3  // (the indexes refer to v, and we rotated it left by 1, so the indices are shifted)
--------- min
[6,6,5,5] v2
 3 2 1 0 // (explained
 2 1 0 3 //  below    )

каждый столбец под элементом v2 трассы какие показатели v были min'D вместе, чтобы получить этот элемент. Итак, идя колонной слева направо:

v2[3] == 6 == min(v[3], v[2])
v2[2] == 6 == min(v[2], v[1])
v2[1] == 5 == min(v[1], v[0])
v2[0] == 5 == min(v[0], v[3])

второе min:

[6,6,5,5] v2
 3 2 1 0
 2 1 0 3
[5,5,6,6] v2_rotated_left_by_2
 1 0 3 2
 0 3 2 1
--------- min
[5,5,5,5] v3
 3 2 1 0
 2 1 0 3
 1 0 3 2
 0 3 2 1

вуаля! Каждая колонка под v3 содержит (3,2,1,0) - каждый элемент v3 была minD со всеми элементами v - таким образом, каждый элемент содержит минимум всего вектора v.

после использования функции вы можете извлечь минимальное значение с помощью float _mm_cvtss_f32(__m128):

__m128 min_vector = _mm_hmin_ps(my_vector);
float minval = _mm_cvtss_f32(min_vector);

***

это просто тангенциальная мысль, но я нашел интересным то, что этот подход может быть расширен до последовательностей произвольной длины, вращая результат предыдущего шага на 1, 2, 4, 8, ... 2**ceil(log2(len(v))) (я думаю) на каждом шаге. Это круто с теоретической точки зрения - если вы можете сравнить две последовательности по элементам одновременно, вы можете найти минимум/максимум1 последовательностей в логарифмическом время!

1 это распространяется на все горизонтальные складки / сокращения, такие как sum. Те же перетасовки, другая вертикальная операция.

однако AVX (256-битные векторы) делает 128-битные границы особенными и сложнее перетасовать. Если вам нужен только скалярный результат, извлеките верхнюю половину, чтобы каждый шаг сужал ширину вектора пополам. (Как в самый быстрый способ сделать горизонтальную сумму вектора поплавка на x86, который имеет более эффективные перетасовки чем 2x shufps для 128-битных векторов, избегая некоторых movaps инструкции при компиляции без AVX.)

но если вы хотите, чтобы результат транслировался на каждый элемент, такой как ответ @PaulR, вы хотели бы сделать перетасовки в полосе (т. е. вращаться в пределах 4 элементов в каждой полосе), а затем поменять местами половинки или повернуть 128-битные полосы.