Как выполнить параллельное сложение с использованием AVX с переносом (переполнением), возвращаемым в тот же элемент (контрольная сумма PE)?

Я хочу выполнить восемь параллельных добавлений 16-битных значений с помощью AVX SIMD. Требуется сложение с переполнением, т. е. «добавление с переносом», как это делается со старой мнемоникой «adc» x86.

Я реализовал 50 процентов решения AVX самостоятельно, обработка переноса, также выполняемая инструкциями AVX, отсутствует. Мое текущее решение:

typedef union _uint16vec uint16vec, *uint16vec_ptr;

union  __attribute__((aligned(16))) _uint16vec
{
  __m128i             x;
  uint16_t            y[8];
};

__m128i parallel_add_with_carry ( __m128i n1, __m128i n2 )
{
  volatile uint16vec  res, stupid_carry;
  uint32_t            i;

  stupid_carry.x = n1; /* load 8x uint16_t for carry adjustment below */

  __asm__
  (
    "movdqa %1, %%xmm0             \n\t"
    "movdqa %2, %%xmm1             \n\t"
    "vpaddw %%xmm0, %%xmm1, %%xmm0 \n\t"
    "movdqa %%xmm0, %0             \n\t"
    : "=m" (res.x)                      /* output */
    : "m" (n1), "m" (n2)                /* inputs */
    : "xmm0", "xmm1"                    /* GCC, please clobber XMM0 and XMM1 */
  );

  /* if each of the eight uint16_t in the result is lesser than
   * the previous value, then we have the overflow situation...
   */
  for (i=0;i<8;i++)
    res.y[i] += (res.y[i] < stupid_carry.y[i]) ? 1 : 0;

  return res.x;
}

void test ( void )
{
  uint16vec   v1 = {0}, v2 = {0}, res;

  v1.y[0] = 0x000A; v2.y[0] = 0x0014; /* 10+20 = 30 (0x001E), no overflow */
  v1.y[1] = 0xFFF0; v2.y[1] = 0x0013; /* 0xFFF0 + 0x0013 = 0x0003 -> overflow -> 0x0004 */

  res.x = parallel_add_with_carry(v1.x, v2.x);

  fprintf(stdout,"%04X | %04X\n", res.y[0], res.y[1]);
}

Объектный код эпилога функции, созданный GCC, ужасен (даже с -O3). Мой вопрос: есть ли лучшее решение с поддержкой AVX для проблемы «добавления с переносом»?

  • Может ли помочь инструкция сравнения 'vpcmp<CC>uw' с <CC>=LT (меньше чем)?
  • Как я могу использовать для этого эти регистры «K» (маски) (K0..K7)?

Моя идея заключалась в том, чтобы предоставить 128-битный вектор { 0x0001,0x0001,...,0x0001} в качестве 128-битной временной переменной, добавив это (вектор переноса) к восьми uint16_t тогда и только тогда, когда предыдущая операция сравнения привела к результату «меньше чем» для конкретного uint16_t в 128-битном векторе.

Я просмотрел документацию Intel и нашел хорошие инструкции по добавлению, которые просто копируют исходные части вектора, если условие выполнено.

Поддержка этой «штуки AVX» высоко ценится. Спасибо.

vpcmpuw это AVX-512. В AVX2 есть только сравнение целых чисел со знаком, хотя у него есть мин/макс без знака и насыщающее вычитание, с которыми вы могли бы что-то сделать. Вы отметили этот AVX2. Вы также ищете его версию, поддерживающую x86-64-v3 (AVX2, но не AVX-512)?
Peter Cordes 20.08.2024 01:23

Почему встроенный ассемблер? То, как вы это пишете, с операндами ввода и вывода в память, неэффективно. Инструкции SIMD x86 допускают использование операнда-источника памяти. И вы не получите никакой выгоды от использования устаревшей кодировки SSE movdqa; если вы хотите сохранить размер кода, избегая кодировки VEX, используйте movaps. Но, вероятно, лучше использовать vmovdqa, хотя при использовании 128-битных векторов вы не получите зависаний при переходе SSE/AVX, пока верхние половины регистров YMM/ZMM чистые (vzeroupper)

Peter Cordes 20.08.2024 01:25

Ваш существующий код выглядит так, как будто он увеличивает элемент res на 1, если было выполнение. Это очень странно; у переноса есть позиционное значение 2^16, но вы возвращаете его обратно в нижнюю часть того же элемента, позиционное значение 2^0. Обычно вам нужно условно увеличить другой вектор, если вы используете восемь параллельных 32-битных счетчиков, каждый из которых разделен на две 16-битные половины.

Peter Cordes 20.08.2024 02:35

Питер, большое спасибо за ваш вклад. Я собираюсь пересмотреть этот материал. Основная идея заключалась в параллельном выполнении восьми 16-битных сложений. Затем, сравнивая с промежуточным результатом до текущего сложения, выясняем, какое из восьми 16-битных значений имело переполнение, исправляя только эти 16-битные значения, добавляя еще одно.

Devvy 20.08.2024 10:33

И да, я хотел бы использовать все виды AVX/AVX-512, чтобы оптимизировать это. Этот код используется при вычислении контрольной суммы исполняемых файлов PE, которые требуют сложения всех 16-битных слов (с переносом). Текущая версия хорошо справляется со своей задачей, но все еще неоптимальна...

Devvy 20.08.2024 10:37

С этой 16-битной контрольной суммой (подача переноса обратно в перенос) остается ли сложение ассоциативным? Я так думаю, иначе вы бы вообще не смогли векторизовать это. Вы можете сократить задержку критического пути, добавив перенос в другой вектор, добавляя его обратно в основной аккумулятор только в конце.

Peter Cordes 20.08.2024 11:11

Вы имеете в виду, что у вас есть все доступные версии AVX1/2/512, и вам нужна одна версия, которая будет максимально быстрой, независимо от того, означает ли это использование AVX2 или AVX-512? Или вы имеете в виду, что вам также нужна версия, использующая только AVX2, и еще одна версия, использующая только AVX1, а также версия, использующая AVX-512BW?

Peter Cordes 20.08.2024 11:12

@Devvy Обратите внимание, что в вашем коде вы неправильно распространяете перенос, поскольку добавление переноса также может привести к переполнению, и это переполнение необходимо учитывать в более высоких элементах.

Andrey Semashev 20.08.2024 13:00

@AndreySemachev: Двойное переполнение невозможно. Рассмотрим 8-битный код с максимально возможными входными данными: 0xFF + 0xFF — это 0xFE с переносом 1. Добавляя его обратно внизу, мы получаем 0xFF без переполнения. Это контрольная сумма, а не сложение bigint, поэтому распространение переноса не происходит в другой элемент.

Peter Cordes 21.08.2024 14:35

@PeterCordes Первоначально я думал, что ОП хочет реализовать обычное сложение с переносом, что означает, что перенос распространяется на следующий, более значимый элемент суммы. Да, без распространения двойное переполнение невозможно.

Andrey Semashev 21.08.2024 17:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
10
126
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

__m128i parallel_add_with_carry(__m128i n1, __m128i n2)
{
    // Make an all-ones vector. This is virtually free and does not
    // consume an execution unit.
    const __m128i mm_all_ones = _mm_cmpeq_epi32(n1, n1);

    __m128i mm_sum = _mm_add_epi16(n1, n2);
    // Since there is no "not equal" comparison that produces
    // a vector mask, we compare for "equal" and get a negated
    // carry mask.
    __m128i mm_carry = _mm_cmpeq_epi16(_mm_adds_epu16(n1, n2), mm_sum);
    // Negate the mask, which turns elements that were 0 to -1 and vise versa
    mm_carry = _mm_xor_si128(mm_carry, mm_all_ones);
    // Add carry (i.e. substract -1 or 0)
    mm_sum = _mm_sub_epi16(mm_sum, mm_carry);

    return mm_sum;
}

Богболт

Эта реализация совместима с SSE2 и более поздними версиями. Использование AVX-512 и регистров opmask возможно, но вряд ли это будет быстрее. Вот некоторые оценки (asm-код, сгенерированный gcc 14.2 с помощью -O3 -march=rocketlake):

Если вы обрабатываете умеренные объемы данных, которые, скорее всего, поместятся в кеш, возможно, будет полезно дополнительно оптимизировать этот код, развернув цикл и используя отдельные аккумуляторы для суммирования и переноса, и складывая их вместе только в конце, когда все данные обрабатывается. Это улучшает параллелизм на уровне команд (ILP), поскольку отдельные накопления образуют отдельные цепочки зависимостей и могут происходить параллельно, при условии, что в ЦП достаточно вычислительных блоков и алгоритм не ограничен где-либо еще (например, пропускной способностью памяти).

__m128i parallel_add_with_carry_loop(const unsigned char* data, size_t size)
{
    __m128i mm_sum1 = _mm_setzero_si128(), mm_sum2 = _mm_setzero_si128();
    __m128i mm_carry1 = _mm_setzero_si128(), mm_carry2 = _mm_setzero_si128();
    const __m128i mm_all_ones = _mm_cmpeq_epi32(mm_sum1, mm_sum1);

    for (size_t i = 0u, n = size & (size_t)(-32); i < n; i += 32)
    {
        __m128i mm1 = _mm_loadu_si128((const __m128i*)(data + i));
        __m128i mm2 = _mm_loadu_si128((const __m128i*)(data + i + 16));
        __m128i mm_new_sum1 = _mm_add_epi16(mm_sum1, mm1);
        __m128i mm_new_sum2 = _mm_add_epi16(mm_sum2, mm2);
        __m128i mm_neg_carry1 = _mm_cmpeq_epi16(
            _mm_adds_epu16(mm_sum1, mm1), mm_new_sum1);
        __m128i mm_neg_carry2 = _mm_cmpeq_epi16(
            _mm_adds_epu16(mm_sum2, mm2), mm_new_sum2);
        mm_sum1 = mm_new_sum1;
        mm_sum2 = mm_new_sum2;
        mm_carry1 = _mm_sub_epi16(mm_carry1,
            _mm_xor_si128(mm_neg_carry1, mm_all_ones));
        mm_carry2 = _mm_sub_epi16(mm_carry2,
            _mm_xor_si128(mm_neg_carry2, mm_all_ones));
    }

    // If size is not a multiple of 32, process the tail bytes here
    // ...

    // Combine the accumulated sum and carry. Note that adding sums
    // may overflow, and we need to account the carry from this addition.
    mm_sum1 = parallel_add_with_carry(mm_sum1, mm_sum2);
    mm_carry1 = _mm_add_epi16(mm_carry1, mm_carry2);
    mm_sum1 = _mm_add_epi16(mm_sum1, mm_carry1);

    return mm_sum1;
}

Так как это для контрольной суммы PE (согласно комментариям), то сдвига влево быть не должно, предполагается добавление переноса элемента к этому самому элементу (и тогда цикл тоже не нужен, так как конец -вокруг керри не может генерировать другой керри)

user555045 20.08.2024 13:36

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

Peter Cordes 20.08.2024 13:52

Если бы вы пытались реализовать 128-битное сложение, вы бы сделали это с помощью двух 64-битных элементов, просто безоговорочно вычитая 0 или -1 из старшей половины. AVX включает vpcmpgtq, чтобы вы могли обойти недостаток насыщенной математики для элементов других размеров. Конечно, SIMD в этом плане не лучше скалярного на x86-64; add/adc более эффективен, чем махинации с AVX, если только вы не можете использовать хранилище частичных слов для своих чисел, чтобы избежать распространения переноса в большинстве операций, перенормируя только время от времени. (Могут ли подпрограммы с длинными целыми числами выиграть от SSE?)

Peter Cordes 20.08.2024 13:55

@user555045 user555045 Тогда я неправильно понял вопрос. Спасибо, что указали на это.

Andrey Semashev 20.08.2024 16:28

Я обновил ответ, чтобы реализовать контрольную сумму PE, а не добавлять с переносом.

Andrey Semashev 21.08.2024 12:28

Можем ли мы избежать _mm_andnot_si128 и просто использовать _mm_sub_epi16? Или невозможно напрямую получить 0 или -1, которые нам нужны, из pcmpeqw или pcmpgtw, полученных с помощью одной предыдущей инструкции?

Peter Cordes 21.08.2024 14:51

@PeterCordes Да, я применил ваше предложение об использовании _mm_sub_epi16. Спасибо. Но я не думаю, что будет какая-либо значительная разница в производительности, если судить по симуляции uica.uops.info. Это сокращает код на 2 мопса и пару инструкций, вот и все. Разница лишь в том, что константу «все единицы» сгенерировать проще, чем вектор из 16-битных. Вам все равно нужно отменить маску переноса, поэтому вы меняете pandn на pxor, что эквивалентно.

Andrey Semashev 21.08.2024 17:05

Если делать это для нескольких векторов, мы могли бы накапливать инвертированные переносы, поэтому нам нужно будет инвертировать их только один раз. (Или sub их из основного векторного аккумулятора вместо add). Позже выносы будут происходить в разных точках, но итог все равно x + (x/65536) Я думаю. Невозможно использовать в API функции-обертки OP, которая делает все для одного вектора, но автономная функция отвлекает от оптимизации цикла, который ее встраивает.

Peter Cordes 21.08.2024 17:14

@PeterCordes Я не уверен, как вы будете накапливать отрицательный перенос. Вам необходимо накопить отрицательные элементы переноса, которые являются нулями, то есть вам нужно сделать эти элементы ненулевыми перед тем, как накапливать так или иначе. На данном этапе имеет ли значение, накапливаете ли вы их отдельно или сразу в конечный результат? Использование отдельного аккумулятора потенциально лучше ILP, но я не уверен, что это будет иметь значение на практике.

Andrey Semashev 21.08.2024 17:22

Я думал pcmpeqw/psubw carries, cmp_result, а затем добавить или заменить переносы один раз в конце. Так что вам не нужно отрицание для каждого pcmp. Это работает? Я не до конца прошел и не опробовал это.

Peter Cordes 21.08.2024 17:37

И да, улучшение ILP от отдельного аккумулятора должно иметь большое значение; Задержка критического пути в 4 цикла для вашей версии (параллельные add и adds) по сравнению с задержкой критического пути в 1 цикл, поэтому просто ограничиваем 3 или 4 векторных ALU-операции за такт (при условии, что внешний интерфейс достаточно широк, чтобы выдерживать нагрузки + накладные расходы цикла) , то есть 5/3 цикла на вектор меньше с 4, для достаточно больших входных блоков, которые неупорядоченный exec не может сильно перекрывать их с окружающим кодом, но где пропускная способность памяти не является узким местом, так что, возможно, уже жарко в L2 кэш.

Peter Cordes 21.08.2024 17:38

@PeterCordes Я не думаю, что это работает, если я правильно понимаю. Опять же, вам нужно увеличить результат, если произошло переполнение (т. е. отрицательный результат переноса/pcmpeqw равен нулю). Вы не должны изменять сумму, если не было переполнения. Например, n1 = 0xFFFF, n2 = 1, промежуточная сумма = n1 + n2 = 0, отрицательный перенос = 0, окончательная сумма = сумма + (отрицательный перенос == 0) = 0 + 1 = 1. Пример 2: n1 = 1 , n2 = 1, промежуточная сумма = 2, отрицательный перенос = 0xFFFF, окончательная сумма = 2.

Andrey Semashev 21.08.2024 17:43

@PeterCordes Я добавил в ответ версию с отдельными аккумуляторами. Ограничивающая задержка в этом случае — это накопление переноса, которое должно быть таким же, как задержка версии без отдельного аккумулятора переноса. Т.е. вам все равно нужно вычислить mm_new_sum, чтобы накопить перенос. Это означает, что весь цикл + последнее сложение (сумма, перенос) должно занять примерно то же время, что и цикл с одним аккумулятором. Я что-то упускаю?

Andrey Semashev 21.08.2024 18:23

@PeterCordes Одна из оптимизаций, которая может быть полезна, - это развернуть цикл и использовать несколько аккумуляторов суммы и переноса. Это разорвало бы цепочку зависимостей для каждого из аккумуляторов. Но тогда нет необходимости накапливать сумму и перенос отдельно - с таким же успехом можно развернуть одноаккумуляторный вариант.

Andrey Semashev 21.08.2024 18:26

Параллельная версия имеет две отдельные зависимости, переносимые в цикле, по 1 такту, одна из которых питает другую цепочкой длиной в несколько тактов. Таким образом, один должен опережать другого на пару тактов, но, что особенно важно, carry не имеет обратной связи с sum, поэтому пропускная способность все равно может составлять 1/такт (или ограничиваться пропускной способностью ALU). Ограничения задержки и пропускной способности процессоров для операций, которые должны выполняться последовательно обсуждается более простой пример с двумя цепочками операций, одна из которых связана с другой.

Peter Cordes 21.08.2024 18:34

@PeterCordes Даже если мы не будем учитывать накопление задержки mm_sum (при условии, что аккумулятор опережает mm_carry), у вас все равно есть _mm_adds_epu16+_mm_cmpeq_epi16+_mm_xor_si128+_mm_sub_epi16 = 4 цикла задержки для расчета mm_carry на каждой итерации цикла. С одним аккумулятором mm_sum у вас будет то же самое плюс _mm_add_epi16, то есть 5 циклов. Однако, напр. при 2x развертывании вы создаете вторую цепочку зависимостей и вдвое уменьшаете общую задержку цикла для каждой из цепочек. Это гораздо больший выигрыш, чем 5->4 цикла на итерацию.

Andrey Semashev 21.08.2024 19:22

@AndreySemachev следующая итерация может начаться до того, как закончится цепочка cmp/xor/sub, add/cmp/xor следующей итерации не зависит от результата подпрограммы

user555045 21.08.2024 20:42

@AndreySemachev: Вы прочитали ответ по ссылке в моем последнем комментарии? Он объясняет то, что обобщил пользователь 555.... (и показывает диаграмму в формате ASCII), что exec, находящийся вне порядка, может начать работу раньше на следующей итерации одной цепочки, поэтому им не нужно работать синхронно.

Peter Cordes 22.08.2024 02:36

Огромное спасибо за такую ​​прекрасную работу и сотрудничество. Ребята, вы потрясающие! Многим людям может быть полезна эта работа, а также выполнение других задач SIMD. Лично я не совсем разбирался в этой области SIMD, но многому научился у вас. Еще раз спасибо.

Devvy 22.08.2024 12:25

Еще один «глупый» вопрос: какой бит CPUID мне нужно проверить на предмет SSE2?

Devvy 22.08.2024 12:30

@Devvy На 64-битной версии x86 вам не нужно ничего проверять, поскольку SSE2 неявно присутствует на любом 64-битном процессоре x86. На старых 32-битных процессорах вы можете проверить бит 26 edx, возвращаемый cpuid, с eax=1. См. Определить поддержку процессора для SSE2?.

Andrey Semashev 22.08.2024 13:12

@PeterCordes Да, я читал ответ, но не осознавал, что _mm_adds_epu16, _mm_cmpeq_epi16 и _mm_xor_si128 не зависят от предыдущей итерации цикла накопления переноса и могут быть выполнены раньше. Комментарий пользователя user555045 вызвал этот щелчок. Спасибо вам обоим.

Andrey Semashev 22.08.2024 13:36

Я нашел решение, используя инструкции AVX-512. Решением является функция маскировки слиянием с использованием регистров маски ЦП (с именами от K0 до K7).

Я тестировал его на серверном сервере с процессором AMD Epyc серии 7000 (48 ядер).

Исходный код C со встроенной ассемблером

В контексте моего первоначального исходного кода:

    __m128i parallel_add_with_carry_simd ( __m128i n1, __m128i n2 )
    {
      static const uint16vec carry = { .y[0] = 0x0001,
                                       .y[1] = 0x0001,
                                       .y[2] = 0x0001,
                                       .y[3] = 0x0001,
                                       .y[4] = 0x0001,
                                       .y[5] = 0x0001,
                                       .y[6] = 0x0001,
                                       .y[7] = 0x0001 };
      uint16vec res;
    
      __asm__
      (
        "movdqa    %1    , %%xmm0                 \n\t"
        "movdqa    %2    , %%xmm1                 \n\t"
        "movdqa    %3    , %%xmm3                 \n\t"
        "movdqa    %%xmm0, %%xmm2                 \n\t"
        "vpaddw    %%xmm1, %%xmm0, %%xmm0         \n\t"
        "vpcmpltuw %%xmm2, %%xmm0, %%k1           \n\t"
        "vpaddw    %%xmm3, %%xmm0, %%xmm0 %{%%k1%}\n\t"
        "movdqa    %%xmm0, %0                     \n\t"
        : "=m" (res.x)
        : "m" (n1), "m" (n2), "m" (carry.x)
        : "xmm0", "xmm1", "xmm2", "xmm3"
      );
    
      return res.x;
    }

По сути, вам нужно выполнить последовательность:

  • Первое сложение восьми 16-битных целых чисел без знака в 128-битный регистр XMM;
  • Выполнение «vpcmpuw» с кодом условия «LT» (меньше);
  • Второе УСЛОВНОЕ добавление вектора переноса (1,1,1,1,1,1,1,1).

Это реализует старую мнемонику x86 «добавление с переносом» «adc» для векторов 16-битных целых чисел без знака.

Прохождение и техническая информация

Встроенная сборка загружает два добавляемых вектора с переносом n1 и n2 в XMM0 и XMM1. Вспомогательный вектор переноса (1,1,1,1,1,1,1,1) загружается в XMM3.

Сохраняем исходный вектор n1 в XMM2 (для последующего сравнения). Расчет n1 + n2 производится (без насыщения!) по формуле:

    XMM0 := XMM0 (n1) + XMM1 (n2)

Этот промежуточный результат еще не включает переносы.

Роль vpcmpuw

Главное — сравнение

    vpcmpltuw %%xmm2, %%xmm0, %%k1

что означает: сравнить промежуточный результат XMM0 с исходным входным значением n1 в XMM2. Используйте код условия «меньше чем» (LT). Сохраните восемь результатов сравнения как отдельные биты в регистре маски K1.

Вторая операция условного добавления

Вторая инструкция vpaddw выполняет работу по «коррекции переноса» восьми 16-битных целых чисел в XMM0:

     vpaddw %%xmm3, %%xmm0, %%xmm0 %{%%k1%}

Это (странное) обозначение означает: добавить вектор переноса (в XMM3) к промежуточному результату (в XMM0), снова сохранив общий результат в XMM0. При добавлении используйте режим слияния-маскирования (в отличие от обнуления-маскирования - см. ниже). Псевдокод маскировки слияния:

    FORALL eight 16bit integers in XMM0 DO
    IF   : bit in K1 is 1 
    THEN : add 0x0001 from the carry vector in XMM3 (XMM0 := XMM0 + XMM3)
    ELSE : just copy the current 16bit integer from src (XMM0) to dst (XMM0)

Что такое обнуление-маскировка?

Альтернативой операции слияния-маскирования является обнуление-маскирование. В этом случае вам придется указать встроенную сборку следующим образом:

    vpaddw %%xmm3, %%xmm0, %%xmm0 %{%%k1%}%{z%}

Если бит в регистре К1 равен единице, то производится сложение. Если это нулевой бит, то в месте назначения сохраняется 0x0000 (таким образом обнуляется текущее 16-битное целое число).

Кредиты

В конце концов, я смог решить эту проблему благодаря этому посту: stackoverflow, маска записи в AVX-512

Вроде бы это сработало бы, но выглядит неэффективно. После того, как это будет встроено в цикл, который вычисляет общую контрольную сумму, все равно будут некоторые избыточные ходы (если вы использовали встроенные функции вместо встроенного ассемблера, компилятор мог бы что-то с этим сделать, но не со встроенным ассемблером). Также кажется возможным сделать это с меньшей задержкой через цепочку зависимостей, переносимую циклом (все еще речь идет о цикле, который здесь не показан, но вам придется вызывать эту функцию из цикла).

user555045 21.08.2024 17:55

Спасибо за ваш вклад. Конечно, я интегрировал это во встроенную секцию ассемблера, которая выполняет цикл как часть it (метка перехода выровнена по 16 байтам), все чередуется настолько, насколько мне это удается — честно говоря, я не знаю, так ли это Материалы о трубопроводах u/v, созданные 20 лет назад, все еще играют роль? Код работает «как шарм», по крайней мере, на AMD Epyc 7000er.

Devvy 21.08.2024 18:43

Проблема с u/v осталась далеко позади, теперь у нас есть внеочередное выполнение, поэтому точное чередование не так важно, как раньше. Я полагаю, что выполнение всего цикла во встроенном ассемблере — это нормально. Тем не менее, я думаю, что последовательность vpaddw/vpcmpltuw/vpaddw имеет задержку в 6 циклов (или, может быть, 5?) в Zen4, поскольку результат привязан обратно к входным данным, что ограничивает выполнение цикла с частотой 1 итерация на 6 циклов, но кажется возможным сделать это быстрее, используя идею Питера Кордеса добавить переносы отдельно (что не связывает вычисления одинаковым образом) и объединить две отдельные суммы после цикла

user555045 21.08.2024 18:56

Спасибо за разъяснения по поводу н/в. Я довольно старый разработчик, 25 лет назад писал ассемблерный код x86 (например, написал расширитель DOS - боже мой, это неловко... :-))

Devvy 21.08.2024 19:02

Другие вопросы по теме