Найдите 4 минимальных значения в 4 регистрах __m256d

Не могу понять как реализовать:

__m256d min(__m256d A, __m256d B, __m256d C, __m256d D)
{
    __m256d result;

    // result should contain 4 minimal values out of 16 : A[0], A[1], A[2], A[3], B[0], ... , D[3]
    // moreover it should be result[0] <= result[1] <= result[2] <= result[2]

     return result;
}

Любые идеи о том, как разумно использовать _mm256_min_pd, _mm256_max_pd и перетасовку/перестановку?

==================================================

Это то, где я зашел так далеко после:

    __m256d T = _mm256_min_pd(A, B);
    __m256d Q = _mm256_max_pd(A, B);
    A = T; B = Q;
    T = _mm256_min_pd(C, D);
    Q = _mm256_max_pd(C, D);
    C = T; D = Q;
    T = _mm256_min_pd(B, C);
    Q = _mm256_max_pd(B, C);
    B = T; C = Q;
    T = _mm256_min_pd(A, B);
    Q = _mm256_max_pd(A, B);
    A = T; D = Q;
    T = _mm256_min_pd(C, D);
    Q = _mm256_max_pd(C, D);
    C = T; D = Q;
    T = _mm256_min_pd(B, C);
    Q = _mm256_max_pd(B, C);
    B = T; C = Q;

имеем: A[0] ‹ B[0] ‹ C[0] ‹ D[0], A[1] ‹ B[1] ‹ C[1] ‹ D[1], A[2] ‹ B[ 2] ‹ C[2] ‹ D[2], A[3] ‹ B[3] ‹ C[3] ‹ D[3],

поэтому минимальное значение находится среди A, второе минимальное значение находится среди A или B, ... Не знаю, куда идти дальше ...

========================================================

Вторая идея заключается в том, что проблема сводится к самой себе, но с двумя входными элементами __m256. Если это можно сделать, то просто выполните min4(A,B) --> P, min4(C,D) --> Q, min4(P,Q) --> возвращаемое значение.

Хотя понятия не имею, как это сделать для двух векторов :)

=======================================================================

Обновление 2: проблема почти решена — следующая функция вычисляет 4 минимальных значения.

__m256d min4(__m256d A, __m256d B, __m256d C, __m256d D)
{
    __m256d T;
    T = _mm256_min_pd(A, B);
    B = _mm256_max_pd(A, B);            
    B = _mm256_permute_pd(B, 0x5);
    A = _mm256_min_pd(T, B);            
    B = _mm256_max_pd(T, B);            
    B = _mm256_permute2f128_pd(B, B, 0x1);
    T = _mm256_min_pd(A, B);
    B = _mm256_max_pd(A, B);
    B = _mm256_permute_pd(B, 0x5);
    A = _mm256_min_pd(A, B);

    T = _mm256_min_pd(C, D);
    D = _mm256_max_pd(C, D);            
    D = _mm256_permute_pd(D, 0x5);
    C = _mm256_min_pd(T, D);            
    D = _mm256_max_pd(T, D);            
    D = _mm256_permute2f128_pd(D, D, 0x1);
    T = _mm256_min_pd(C, D);
    D = _mm256_max_pd(C, D);
    D = _mm256_permute_pd(D, 0x5);
    C = _mm256_min_pd(C, D);

    T = _mm256_min_pd(A, C);
    C = _mm256_max_pd(A, C);            
    C = _mm256_permute_pd(C, 0x5);
    A = _mm256_min_pd(T, C);            
    C = _mm256_max_pd(T, C);            
    C = _mm256_permute2f128_pd(C, C, 0x1);
    T = _mm256_min_pd(A, C);
    C = _mm256_max_pd(A, C);
    C = _mm256_permute_pd(C, 0x5);
    A = _mm256_min_pd(A, C);

    return A;
};

Остается только отсортировать значения в порядке возрастания внутри A перед возвратом.


person Fedor_Govnjukoff    schedule 11.03.2016    source источник
comment
Какова конкретная проблема, с которой вы столкнулись? Это довольно широкий вопрос.   -  person Adam B    schedule 11.03.2016
comment
Вы ищете тот, который собирает наименьшие 4 двойника из всех 16 двойников в один вектор, по порядку, верно? Сеть сортировки Google SIMD и тому подобное. Вы можете обнаружить, что распаковка в два вектора __m128d полезна для некоторых шагов, а может и нет. Если вас интересуют только 4 наименьших элемента, а не полная сортировка, может быть сложнее превзойти скалярный код с помощью сети сортировки SIMD.   -  person Peter Cordes    schedule 11.03.2016
comment
Верно -- 4 младших двойника из всех 16 двойников образуют один вектор. Эти 4 вектора содержат 16 значений, которые являются результатом вычислений SIMD, которые работают очень хорошо. В конце должны быть выбраны 4 младших. Цель состоит не в том, чтобы превзойти скалярный код, а просто в том, чтобы избежать его. Мне кажется нелогичным выгружать значения в память, потом делать сортировку, потом снова загружать.   -  person Fedor_Govnjukoff    schedule 11.03.2016
comment
Какой здесь самый важный критерий эффективности? Задержка, пропускная способность или общее количество операций с объединенным доменом (т. е. влияние на пропускную способность окружающего кода)? Может ли выполнение не по порядку потенциально иметь несколько сортировок одновременно или перекрываться с другой работой, или это часть зависимости, переносимой циклом?   -  person Peter Cordes    schedule 12.03.2016
comment
Кстати, формальное название для поиска наименьших k элементов из n — алгоритм выбора. (технически алгоритм выбора находит только статистику k-го порядка, а не все k..n или 0..k (частичная сортировка). Нам нужна частичная сортировка, которая не требует дополнительной работы, чтобы убедиться, что остальная часть массива ( или регистры) по-прежнему содержат значимые данные.) Во всяком случае, я не нашел большого обсуждения очень маленького k, где n также мало, когда гуглил алгоритм выбора simd. :/   -  person Peter Cordes    schedule 12.03.2016
comment
Если бы вы не делали этого вертикально, это было бы намного проще - позволит ли ваш вариант использования выполнять 4 такие операции параллельно?   -  person Paul R    schedule 12.03.2016


Ответы (2)


Возможно, было бы лучше сделать некоторые сравнения SIMD, чтобы уменьшить до 8 или 4 (как у вас сейчас) кандидатов, а затем распаковать в скалярные двойники в векторных регистрах. Это не должно включать обход памяти: vextractf128 старшая половина (_mm256_extractf128_pd) и преобразование младшей половины. Возможно, используйте movhlps (_mm_movehl_ps), чтобы преобразовать старшую половину __m128 в младшую половину (хотя на процессорах с AVX вы экономите только один или два байта кода от использования этого вместо случайного перемешивания с немедленным; это не быстрее, чем это есть на некоторых старых процессорах).

IDK, распаковка с перетасовкой или просто сохранение - это путь. Возможно, сочетание того и другого, чтобы порты перемешивания и порты сохранения / загрузки были заняты, было бы хорошо. Очевидно, что младший двойник в каждом векторе уже присутствует как скаляр, так что вам не нужно его загружать. (И компиляторы плохо разбираются в том, как хранить и перезагружать как скаляры, чтобы воспользоваться этим, даже для локального массива.)

Даже не сильно сужая набор кандидатов, некоторые SIMD-компараторы перед распаковкой могут уменьшить количество перестановок/перетасовок, ожидаемых от разветвленного скалярного кода, уменьшая штрафы за неправильное предсказание ветвления.


Как я описал в комментариях к ответу Пола Р., в скалярном коде вы, вероятно, преуспеете с алгоритмом сортировки вставкой. Но это больше похоже на приоритетную очередь: вставлять только в первые 4 элемента. Если новый кандидат больше, чем самый большой существующий кандидат, просто двигайтесь дальше. В противном случае отсортируйте его вставкой в ​​​​список из 4 кандидатов, которые вы поддерживаете в отсортированном порядке.


Я нашел действительно хорошую статью о сетях сортировки SIMD с конкретными обсуждение AVX. Они подробно рассказывают о необходимых перетасовках при использовании SIMD-инструкций Packed-min/Packed-Max для сортировки пары векторных регистров данных. Они даже используют встроенные функции, такие как _mm512_shuffle_epi32, в своих примерах. Они говорят, что их результаты применимы к AVX, хотя в своих примерах они используют регистры маски AVX-512.

Это только последний фрагмент статьи, где они говорят о слиянии, чтобы использовать маленькую сортировку в качестве строительного блока для большой параллельной сортировки. Я нигде не могу найти их фактический код, поэтому, возможно, они никогда не публиковали полную реализацию, которую они сравнивали для построения своих графиков. :(

Кстати, я написал предыдущий ответ с некоторыми не очень хорошими идеями о сортировке 64-битных структур по члену float, но это здесь это не совсем применимо, поскольку я рассматривал только сложности работы с полезной нагрузкой (которой у вас нет).


У меня сейчас нет времени, чтобы закончить этот ответ, поэтому я просто опубликую краткое изложение своей идеи:

Адаптируйте двухрегистровый метод из этого документа к AVX (или AVX2). Я не уверен, как лучше всего эмулировать их минимальные/максимальные инструкции AVX512 в маске. :/ Я могу обновить это позже. Возможно, вы захотите написать авторам по электронной почте и спросить о коде, который они использовали для тестирования процессора настольного компьютера.

В любом случае, используйте функцию 2 регистров на парах регов, чтобы уменьшить с 4 до 2 рег, а затем снова уменьшить до 1 рег. В отличие от вашей версии, их версия создает полностью отсортированный выходной регистр.

Попытка избежать перетасовки между дорожками, когда это возможно, может быть сложной задачей. Я не уверен, что вы можете получить что-то от использования shufpd (__m256d _mm256_shuffle_pd (__m256d a, __m256d b, const int select);) для объединения данных из двух исходных регистров во время перетасовки. Версия 256b может делать разные перетасовки на каждой дорожке, используя 4 бита imm8 вместо 2.

Это интересная проблема, но я, к сожалению, не должен тратить время на то, чтобы написать полное решение самостоятельно. Если бы у меня было время, я бы хотел сравнить приоритетную очередь сортировки вставками и полностью развернутую реализацию той же очереди сортирующей сети с 4, 8, 12 и 16 элементами в каждой. (разные уровни сети сортировки SIMD, прежде чем перейти к скаляру).

Ссылки, которые я нашел:

person Peter Cordes    schedule 12.03.2016
comment
Хороший ответ! Я добавлю еще одну ссылку на статью, которая, на мой взгляд, выглядит многообещающе. Он получил псевдокод на странице 6 (1279) vldb.org/pvldb/vol8. /p1274-inoue.pdf - person gustf; 13.03.2016
comment
@gustf: не просто псевдокод: настоящий C++ со встроенными функциями. Интересно: я постоянно забываю о palignr для объединения элементов из двух векторов. Конечно, этот вопрос касается float, поэтому palignr вызовет дополнительную задержку при пересылке на minpd/maxpd. Они используют его для передачи одного элемента, поэтому, к сожалению, он не соответствует _mm256_permute2f128_pd. - person Peter Cordes; 13.03.2016
comment
Правда, я хотел написать встроенные функции, не знаю, что произошло на самом деле :) И да, вы правы насчет alignr, но я подумал, что реальный алгоритм может представлять интерес. - person gustf; 13.03.2016

Это чисто "горизонтальная" операция и не очень подходит для SIMD - я подозреваю, что будет быстрее просто спрятать четыре вектора в памяти, отсортировать 16 значений, а затем загрузить первые четыре в результирующий вектор:

__m256d min(__m256d A, __m256d B, __m256d C, __m256d D)
{
    double buff[16] __attribute__ ((aligned(32)));

    _mm256_store_pd(&buff[0], A);
    _mm256_store_pd(&buff[4], B);
    _mm256_store_pd(&buff[8], C);
    _mm256_store_pd(&buff[12], D);

    std::partial_sort(buff, buff+4, buff+16);

    return _mm256_load_pd(&buff[0]);    
}

Для повышения производительности вы можете реализовать встроенную пользовательскую процедуру сортировки, которая жестко закодирована для 16 элементов.

person Paul R    schedule 11.03.2016
comment
Я знаю об этом очевидном решении, сэр. - person Fedor_Govnjukoff; 11.03.2016
comment
ОК - тогда вы можете добавить эту информацию к своему вопросу. - person Paul R; 11.03.2016
comment
используйте std::partial_sort(buff, buff+4, buff+16), чтобы не тратить время на сортировку всего массива. - person Peter Cordes; 12.03.2016
comment
@PeterCordes: спасибо - хорошее предложение - я даже не знал, что такое существует. - person Paul R; 12.03.2016
comment
Я не был уверен, что для этого существует функция STL, пока не посмотрел, но я знал, что такая концепция существует. Надеюсь, у него есть другие стратегии, когда диапазон частичной сортировки очень мал. например сортировка вставками по первым 4 элементам, а затем остановка проверки. Или поддерживайте очередь из 4 самых высоких элементов, увиденных до сих пор. std::partial_sort по-прежнему выполняет больше работы, чем необходимо, потому что остальная часть массива не может быть повреждена (например, повторяющиеся копии элементов). Может быть, есть функция STL, которая даже лучше подходит для этого, но partial_sort был тем, что я нашел первым. - person Peter Cordes; 12.03.2016
comment
Кроме того, с -std=gnu++11 вы можете использовать alignas(32) double buff[16];. Затем gcc и clang генерируют необходимый and rsp, -32 или аналогичный после настройки кадра стека. К сожалению, это выглядит не так эффективно, как могло бы быть :/ Возможно, что-то с векторной сортировкой сети, а затем скалярная горизонталь из 4 или 8 элементов лучше. - person Peter Cordes; 12.03.2016
comment
@PeterCordes: я думаю, что если мы хотим выгрузить значения в массив памяти и частично отсортировать их, то самое простое, что нужно сделать, это запустить цикл, подобный пузырьковой сортировке, 4 раза сверху вниз + i массива, каждый раз нажимая нижний элемент вниз. Это потребует примерно 15 + 14 + 13 + 12 = 54 операций сравнения и обмена, если больше. - person Fedor_Govnjukoff; 12.03.2016
comment
@Fedor_Govnjukoff: сортировка вставками считается оптимальной для небольших массивов. - person Paul R; 12.03.2016
comment
@PaulR: Возможно, сортировка вставками быстрее для небольших массивов, но как адаптировать ее для поиска 4 нижних элементов, неясно. - person Fedor_Govnjukoff; 12.03.2016
comment
@Fedor_Govnjukoff: правда - я никогда этого не пробовал. Сетевые сортировки также могут быть такими же быстрыми для небольших массивов, особенно если у вас есть min/max без ветвей, и вы можете сократить сеть сортировки, чтобы она генерировала только 4 наименьших выхода. - person Paul R; 12.03.2016
comment
@Fedor_Govnjukoff: адаптация сортировки вставками: нормально работать с первыми 4 элементами, поэтому они отсортированы. После этого рассмотрите возможность вставки только в первые 4 элемента. При смещении, чтобы освободить место для нового элемента, вы можете прекратить смещение после записи нового 4-го элемента. Таким образом, вы в основном рассматриваете первые 4 элемента как приоритетную очередь, в которую вы вставляете. Для каждого следующего элемента сначала проверьте самый большой элемент в очереди, чтобы увидеть, меньше ли он, чем любой из текущих min4. На процессоре Intel с низкой задержкой gp‹-›vector, возможно широковещательная нагрузка и cmpps-›movmsk-›bsf - person Peter Cordes; 12.03.2016