Дана серия из пары 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
), кажется слишком перегруженным mov
s: https://godbolt.org/z/cc9v1846n
Это нормально и не влияет ли это на производительность? На какое улучшение производительности я могу рассчитывать, если перепишу его, например, на встроенная сборка с ручным управлением регистром?
Я не знаю, действительно ли необходимы все эти перетасовки, но у меня возникла одна мысль: сдвинуть оба аргумента вправо на 1 бит, вычислить 16-битную сумму и добавить ее, если оба исходных аргумента были нечетными. Тогда вы сможете сделать вдвое больше за один раз.
@SimonGoater мне нужен последний кусочек
Прежде всего, вам необходимо скомпилировать с включенной оптимизацией, иначе сгенерированный компилятором 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
@TomilovAnatoliy: Да, выглядит неплохо. Всего один vpermq
на vpackssdw
из двух vpmaddwd
/vpsrad
результатов. Особенно, если вы хотите округлить в сторону -бесконечности, как будто вы получаете арифметический сдвиг вправо без vpaddd
, вероятно, трудно превзойти это с помощью vpavgw
, поскольку процессоры имеют эффективную поддержку madd.
К сожалению, clang использует режим индексированной адресации, поскольку в противном случае vpmaddwd
мог бы объединиться в одну операцию load+madd на Intel. (Режимы микрослияния и адресации / uops.info)
Ваша ссылка скомпилирована без каких-либо оптимизаций. Добавьте флаг
-O2
или-O3
, чтобы включить оптимизацию. Конечно, если вы добавите-O3
, вы можете обнаружить, что компилятор сделает всю эту векторизацию за вас (хорошо, это не совсем то же самое, но близко).