Эффективно вычислять средние значения горизонтальных пар в потоке int16

Дана серия из пары int16_t. Первый элемент в каждой паре — это семпл левого звукового канала, второй — правого. Я хочу сделать их моно: mono = (left + right) / 2 и не хочу потерять ни капельки.
Следующая программа делает то, что я хочу (я почти уверен):

#include <type_traits>
#include <cstdint>

#include <fmt/format.h>
#include <fmt/ranges.h>

#include <x86intrin.h>

int main()
{
    constexpr auto step = sizeof(__m128i) / sizeof(uint16_t);
    alignas(__m128i) uint16_t input[4 * step];
    uint16_t i = 0;
    for (uint16_t & x : input) {
        x = 1 + 2 * i++;
    }
    alignas(__m256i) uint16_t result[std::extent_v<decltype(input)> / 2];
    for (size_t i = 0; i < std::extent_v<decltype(input)>; i += 4 * step) {
        __m256 vec0 = _mm256_cvtepi16_epi32(_mm_load_si128((const __m128i *)(input + i + 0 * step)));
        __m256 vec1 = _mm256_cvtepi16_epi32(_mm_load_si128((const __m128i *)(input + i + 1 * step)));
        __m256i sum01 = _mm256_hadd_epi32(vec0, vec1);
        __m256i mean01 = _mm256_srai_epi32(_mm256_permute4x64_epi64(sum01, _MM_SHUFFLE(3, 1, 2, 0)), 1);

        __m256 vec2 = _mm256_cvtepi16_epi32(_mm_load_si128((const __m128i *)(input + i + 2 * step)));
        __m256 vec3 = _mm256_cvtepi16_epi32(_mm_load_si128((const __m128i *)(input + i + 3 * step)));
        __m256i sum23 = _mm256_hadd_epi32(vec2, vec3);
        __m256i mean23 = _mm256_srai_epi32(_mm256_permute4x64_epi64(sum23, _MM_SHUFFLE(3, 1, 2, 0)), 1);

        _mm256_store_si256((__m256i *)(result + i / 2), _mm256_permute4x64_epi64(_mm256_packs_epi32(mean01, mean23), _MM_SHUFFLE(3, 1, 2, 0)));
    }
    fmt::println("{}", fmt::join(result, ", "));
}

Но код, сгенерированный clang из багажника (для -mavx2), кажется слишком перегруженным movs: https://godbolt.org/z/cc9v1846n

Это нормально и не влияет ли это на производительность? На какое улучшение производительности я могу рассчитывать, если перепишу его, например, на встроенная сборка с ручным управлением регистром?

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

Miles Budnek 09.06.2024 23:27

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

Simon Goater 09.06.2024 23:50

@SimonGoater мне нужен последний кусочек

Tomilov Anatoliy 10.06.2024 00:07
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
3
98
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

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


Вы могли бы использовать pmaddwd (_mm256_madd_epi16) с постоянным множителем set1_epi16(1), чтобы получить 32-битные суммы горизонтальных пар с помощью одного мопа вместо двух преобразований и инструкции из трех мопов hadd (2 перетасовки плюс вертикальное сложение: https://uops.info/)

Это дает вам переменную __m256i sum01 из вашей версии (из одной 256-битной загрузки и _mm256_madd_epi16(v, _mm256_set1_epi16(1)), за исключением элементов в порядке, вместо внутреннего поведения 256-битной hadd. Поэтому после этого упаковываем ее обратно в 16-битные элементы. переключение нельзя просто использовать vpackssdw.


Другой вариант:
pavgw работает вертикально, но вы, вероятно, можете создать для него 2 входа с меньшими усилиями, чем требуется для расширения и перемешивания. Но _mm256_avg_epu16 работает с беззнаковыми 16-битными целыми числами, и вам нужен знак. Вы можете сдвинуть диапазон к беззнаковому с помощью XOR с 0x8000 (т. е. вычитая INT16_MIN), а затем сделать то же самое со средним беззнаковым числом, чтобы сдвинуть его обратно.

pavgw делает (x + y + 1) >> 1 более похожим на округление до ближайшего, а не на усечение при делении на 2.


В зависимости от того, что вам нужно/хотите, я не уверен, какой из vpmaddwd или vpavgw окажется более эффективным; Хитрость заключалась бы в оптимизации перетасовки при пересечении полосы движения до и/или после.

Мне не удалось реализовать случай pavgw. А вот pmaddwd выглядит красиво: godbolt.org/z/589e4EG45

Tomilov Anatoliy 10.06.2024 11:36

@TomilovAnatoliy: Да, выглядит неплохо. Всего один vpermq на vpackssdw из двух vpmaddwd/vpsrad результатов. Особенно, если вы хотите округлить в сторону -бесконечности, как будто вы получаете арифметический сдвиг вправо без vpaddd, вероятно, трудно превзойти это с помощью vpavgw, поскольку процессоры имеют эффективную поддержку madd.

Peter Cordes 10.06.2024 23:00

К сожалению, clang использует режим индексированной адресации, поскольку в противном случае vpmaddwd мог бы объединиться в одну операцию load+madd на Intel. (Режимы микрослияния и адресации / uops.info)

Peter Cordes 10.06.2024 23:04

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