горизонтальная сумма 8 упакованных 32-битных чисел с плавающей запятой

Если у меня есть 8 упакованных 32-битных чисел с плавающей запятой (__m256), какой самый быстрый способ извлечь горизонтальную сумму всех 8 элементов? Аналогично, как получить горизонтальный максимум и минимум? Другими словами, какова наилучшая реализация следующих функций C++?

float sum(__m256 x);  ///< returns sum of all 8 elements
float max(__m256 x);  ///< returns the maximum of all 8 elements
float min(__m256 x);  ///< returns the minimum of all 8 elements

person Walter    schedule 14.12.2012    source источник
comment
Вот ссылка на предыдущий вопрос о вычислении горизонтальной суммы упакованных doubles. Вы также должны быть в состоянии адаптировать его к вашему float делу. Это наиболее эффективно, если у вас есть несколько элементов __m256, сумму которых вы хотите вычислить параллельно.   -  person Jason R    schedule 14.12.2012
comment
@JasonR извините, но это не помогает: это совсем другая проблема.   -  person Walter    schedule 14.12.2012
comment
Как это совсем другое? Вам нужно будет использовать горизонтальные добавления и перестановки, чтобы выстроить термины, которые вы хотите добавить, как показано в другом вопросе. Вы также можете использовать аналогичную структуру для операций min и max. Я понимаю, что это не полный ответ (отсюда и комментарий), но он должен помочь вам начать.   -  person Jason R    schedule 14.12.2012
comment
@JasonR ну да, это не совсем бесполезно, но есть много похожих проблем, в которых все используют перетасовку и перестановку в сочетании с горизонтальными и вертикальными операциями. Кстати, нет горизонтального мин/макс, не так ли?   -  person Walter    schedule 14.12.2012
comment
Я не знаю горизонтального мин/макс операции. Один из методов, который может одновременно обеспечить минимальное и максимальное значение, заключается в использовании сети сортировки внутри регистра для сортировки элементов внутри регистра SIMD. Алгоритм, подходящий для реализации на __m128, можно найти в этой статье. ; требуется ~ 15 инструкций или около того. То, как регистры YMM реализованы на x86, вероятно, усложняет задачу сортировки __m256, поскольку по большей части вы не можете пересечь 128-битную границу.   -  person Jason R    schedule 14.12.2012
comment
@PeterCordes Я вижу, что вы использовали библиотеку векторных классов C++ Agner Fog. Это хорошо работает? На самом деле у меня есть своя аналогичная (но гораздо менее обширная) библиотека и думаю, не лучше ли от нее отказаться в пользу Туманной.   -  person Walter    schedule 10.11.2016
comment
@Walter: я мало им пользовался. Я некоторое время работал над его улучшением, но так и не вернулся к очистке моих изменений в приятных коммитах git. Тем не менее, он обычно компилируется в хороший ассемблер и выглядит хорошо спроектированным. Я определенно рекомендую его для проектов, в которых можно использовать библиотеку GPL. (Это полная GPL, а не LGPL, поэтому ее могут использовать только проекты, совместимые с GPL.)   -  person Peter Cordes    schedule 10.11.2016


Ответы (4)


Быстро записал здесь (и, следовательно, непроверенный):

float sum(__m256 x) {
    __m128 hi = _mm256_extractf128_ps(x, 1);
    __m128 lo = _mm256_extractf128_ps(x, 0);
    lo = _mm_add_ps(hi, lo);
    hi = _mm_movehl_ps(hi, lo);
    lo = _mm_add_ps(hi, lo);
    hi = _mm_shuffle_ps(lo, lo, 1);
    lo = _mm_add_ss(hi, lo);
    return _mm_cvtss_f32(lo);
}

Для мин/макс замените _mm_add_ps и _mm_add_ss на _mm_max_* или _mm_min_*.

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

person Stephen Canon    schedule 17.12.2012

Хотя ответ Стивена Кэнона, вероятно, идеален для нахождения горизонтального максимума/минимума, я думаю, что для горизонтальной суммы можно найти лучшее решение.

float horizontal_add (__m256 a) {
    __m256 t1 = _mm256_hadd_ps(a,a);
    __m256 t2 = _mm256_hadd_ps(t1,t1);
    __m128 t3 = _mm256_extractf128_ps(t2,1);
    __m128 t4 = _mm_add_ss(_mm256_castps256_ps128(t2),t3);
    return _mm_cvtss_f32(t4);        
}
person Z boson    schedule 04.09.2013
comment
Обратите внимание, что VHADDPS имеет задержку в 5 циклов на Sandy Bridge/Ivy Bridge, поэтому вполне возможно, что это на самом деле может быть менее эффективным, чем реализация Стивена Кэнона (где все инструкции обычно имеют задержку в 1 цикл). - person Paul R; 04.08.2015
comment
@PaulR, возможно, ты прав. Но в любом случае горизонтальные операции не должны выполняться на каждой итерации критического цикла. - person Z boson; 20.08.2015
comment
Конечно, я просто подумал, что стоит отметить, что это может быть один из тех нелогичных случаев, когда меньшее количество инструкций может не соответствовать более быстрому выполнению. - person Paul R; 20.08.2015
comment
@PaulR, я признаю, что в прошлом я слишком много внимания уделял количеству инструкций. В настоящее время я бы посмотрел на общую задержку и пропускную способность и использовал что-то вроде IACA (и протестировал в своем приложении). Но в любом случае я думаю, что изначально придумал это решение из VCL Агнера Фога (именно так я изучил SSE и AVX). Если бы вам пришлось сделать ставку на решение между Агнер Фог и Стивен Кэнон, как бы вы поспорили? Думаю, я бы подбросил монетку. - person Z boson; 28.09.2015
comment
Я ошибался слишком много раз, чтобы делать ставки на такие вещи. ;-) В основном я склоняюсь к эмпирическому подходу - зачем спекулировать, когда можно сравнить? - person Paul R; 28.09.2015
comment
@PaulR, поздравляю с золотым тегом Simd! - person Z boson; 27.05.2016
comment
@PaulR, кстати, я подозреваю, что несколько дней назад Агнер проголосовал против моего ответа и проголосовал за Стивена. Думаю, ему не понравилась моя шутка или, по крайней мере, он дал понять, на кого будет ставить. - person Z boson; 27.05.2016
comment
Я думаю, любой голос Агнера, будь он за или против, будет почетным знаком! (Все равно проголосуйте за меня, в качестве компенсации.) - person Paul R; 27.05.2016
comment
@Zboson: это был мой отрицательный голос, потому что я думаю, что идиома хадда обычно неправильный выбор. Я нашел этот ответ, работая над очисткой того ответа, который я только что связал. Очень трудно сказать, что один способ всегда лучше, и требуется очень много времени, чтобы изучить преимущества и недостатки разных способов сделать одно и то же. Однако в этом случае, если вы не начнете с extract128, это явно ухудшит ситуацию (например, для бульдозера) без какой-либо пользы для других. - person Peter Cordes; 27.05.2016
comment
@PeterCordes, в этом случае вы можете написать Agner Fog (снова), так как я скопировал его код из VCL. См. файл vectorf256.h и функцию horizontal_add для Vec8f. Я знаю, что hadd это плохо. На самом деле эта тема была моим первым вопросом. на SO. - person Z boson; 30.05.2016
comment
@Zboson: см. agner.org/optimize/vectorclass/read.php? я=124. Я внес некоторые улучшения в целочисленные hsums. - person Peter Cordes; 03.06.2016
comment
@PeterCordes, здорово, что ты это делаешь! VCL станет лучше благодаря вам. Но зачем оптимизировать горизонтальные операции? Горизонтальные операции в критическом цикле обычно указывают на неэффективную реализацию. Я думаю, что оптимизация 64-битного мульта имеет больше смысла, потому что я могу себе представить, что это нужно. О, и поздравляю с золотым тегом x86! - person Z boson; 04.06.2016
comment
@Zboson: я оптимизирую hsums, потому что они были первым, что я увидел, что было явно неоптимальным. Я еще не рассматривал другие сложные функции (например, метапрограммирование шаблона для выбора перетасовки). И да, с нетерпением жду возможности использовать дуп-молот для хорошего эффекта. Теперь действительно будет казаться, что стоит искать дубликаты плохих вопросов. - person Peter Cordes; 04.06.2016

Я попытался написать код, который позволяет избежать смешивания инструкций avx и не-avx, а горизонтальная сумма регистра avx, содержащего числа с плавающей запятой, может быть выполнена только avx с помощью

  • 1x vperm2f128,
  • 2x vshufps и
  • 3x vaddps,

в результате получается регистр, в котором все записи содержат сумму всех элементов исходного регистра.

// permute
//  4, 5, 6, 7, 0, 1, 2, 3
// add
//  0+4, 1+5, 2+6, 3+7, 4+0, 5+1, 6+2, 7+3
// shuffle
//  1+5, 0+4, 3+7, 2+6, 5+1, 4+0, 7+3, 6+2
// add
//  1+5+0+4, 0+4+1+5, 3+7+2+6, 2+6+3+7, 
//  5+1+4+0, 4+0+5+1, 7+3+6+2, 6+2+7+3
// shuffle
//  3+7+2+6, 2+6+3+7, 1+5+0+4, 0+4+1+5, 
//  7+3+6+2, 6+2+7+3, 5+1+4+0, 4+0+5+1
// add
//  3+7+2+6+1+5+0+4, 2+6+3+7+0+4+1+5, 1+5+0+4+3+7+2+6, 0+4+1+5+2+6+3+7,
//  7+3+6+2+5+1+4+0, 6+2+7+3+4+0+5+1, 5+1+4+0+7+3+6+2, 4+0+5+1+6+2+7+3

static inline __m256 hsums(__m256 const& v)
{
    auto x = _mm256_permute2f128_ps(v, v, 1);
    auto y = _mm256_add_ps(v, x);
    x = _mm256_shuffle_ps(y, y, _MM_SHUFFLE(2, 3, 0, 1));
    x = _mm256_add_ps(x, y);
    y = _mm256_shuffle_ps(x, x, _MM_SHUFFLE(1, 0, 3, 2));
    return _mm256_add_ps(x, y);
}

Затем получить значение легко, используя _mm256_castps256_ps128 и _mm_cvtss_f32:

static inline float hadd(__m256 const& v)
{
    return _mm_cvtss_f32(_mm256_castps256_ps128(hsums(v)));
}

Я провел несколько базовых тестов по сравнению с другими решениями с __rdtscp и не нашел ни одного лучшего с точки зрения среднего количества циклов процессора на моем Intel i5-2500k.

Просматривая таблицы инструкций Agner, я обнаружил (для процессоров Sandy-Bridge):

                µops    lat.    1/tp    count

this:

vperm2f128      1       2       1       1
vaddps          1       3       1       3
vshufps         1       1       1       2

sum             6       13      6       6

Z boson:

vhaddps         3       5       2       2
vextractf128    1       2       1       1
addss           1       3       1       1

sum             8       15      6       4

Stephen Canon:

vextractf128    1       2       1       1
addps           1       3       1       2
movhlps         1       1       1       1
shufps          1       1       1       1
addss           1       3       1       1

sum             8       13      6       6

где для меня (из-за того, что значения довольно похожи) ни один из них явно не лучше (поскольку я не могу предвидеть, что имеет наибольшее значение: количество инструкций, количество микроопераций, задержка или пропускная способность). редактировать, примечание: потенциальная проблема, которая, как я предполагал, существует в следующем, не соответствует действительности. Я подозревал, что - если достаточно иметь результат в регистре ymm - мой hsums может быть полезен, поскольку он не t требует vzeroupper для предотвращения штрафа за переключение состояний и, таким образом, может чередоваться/выполняться одновременно с другими вычислениями avx с использованием разных регистров без введения какой-либо точки последовательности.

person Pixelchemist    schedule 09.11.2016
comment
__m128 встроенные функции по-прежнему используют версию AVX с 3 операндами, закодированную VEX, когда вы компилируете с включенной поддержкой AVX. Вы правы в том, что ABI требует, чтобы автономная версия float hsum(__m256) включала VZEROUPPER, но вы всегда хотите, чтобы она всегда была встроенной. В SysV ABI все регистры XMM/YMM/ZMM затираются вызовами, поэтому вызывающему объекту придется проливать все, независимо от того, возвращает ли функция __m256 или число с плавающей запятой. (И в Windows есть только несколько регистров XMM с сохранением вызовов, и это только младшие половины, без регистров YMM с сохранением вызовов.) - person Peter Cordes; 09.11.2016
comment
@PeterCordes: Произойдет ли разлив, несмотря на встраивание? - person Pixelchemist; 09.11.2016
comment
Нет, это одно из основных преимуществ встраивания! - person Peter Cordes; 09.11.2016
comment
В подсчете ответа Стивена Кэнона вы пропустили VEXTRACTF128 верхней половины. Обе ваши функции должны быть эквивалентны: одна перетасовка на пересечении дорожки и две перетасовки на дорожке, а также 3 добавления FP. За исключением того, что процессор Стивена будет работать быстрее на семействе AMD Bulldozer или других процессорах с исполнительными блоками всего 128 байт (поэтому vaddps ymm, ymm, ymm медленнее, чем vaddps xmm, xmm, xmm). - person Peter Cordes; 09.11.2016
comment
См. также мой ответ hsums, где Раздел AVX использует vextractf128, vmovshdup и vmovhlps, что эквивалентно Стивену, но сохраняет байт инструкции, потому что для этих перетасовок не требуется управляющий операнд imm8. - person Peter Cordes; 09.11.2016
comment
Ваш способ - правильный подход, если полезно, чтобы hsum транслировался каждому элементу __m256. Иногда это может быть полезно, и делать это таким образом определенно лучше, чем использовать отдельный VBROADCASTSS (особенно без AVX2, поскольку версия AVX1 существует только как загрузка). На процессоре, где операции YMM декодируются до 2 м/операций (например, Bulldozer), возможно, стоит поискать альтернативу. Например, возможно, транслировать результат через __m128, а затем дублировать его на верхнюю полосу. - person Peter Cordes; 09.11.2016
comment
@PeterCordes: Большое спасибо за ваши поучительные комментарии. - person Pixelchemist; 09.11.2016

person    schedule
comment
пожалуйста, добавьте объяснение и детализируйте, как вы полагаете, что это будет ответом на вопрос - person njzk2; 03.06.2014