Нашел решение. Комментарий Питера Кордеса подтолкнул меня в правильном направлении. Что-то с моим тестом было не так. Я использую бенчмарк Google, и вот исходный код бенчмарка, который я использовал:
#include <benchmark/benchmark.h>
#include <x86intrin.h>
#include <array>
class FixtureBenchmark_m256 : public benchmark::Fixture
{
public:
std::array<std::array<__m256, 8>, 10000> in;
std::array<std::array<__m256, 8>, 10000> out;
FixtureBenchmark_m256()
{
__m256 tmp0 = _mm256_setr_ps(1, 2, 3, 4, 5, 6, 7, 8);
for (std::size_t i = 0; i < 1000; ++i)
for (std::size_t j = 0; j < 8; ++j)
{
__m256 tmp1 = _mm256_set1_ps(i * 8 + j);
in[i][j] = _mm256_mul_ps(tmp0, tmp1);
}
}
};
void T7x4_assign(__m256 in0, __m256 in1, __m256 in2, __m256 in3, __m256& out0, __m256& out1, __m256& out2, __m256& out3,
__m256& out4, __m256& out5, __m256& out6)
{
__m256 tout0, tout1, tout2, tout3, tout4, tout5, tout6;
__m256 tmp0, tmp1, tmp2, tmp3;
__m256 tmp4 = _mm256_unpacklo_ps(in3, in0);
__m256 tmp5 = _mm256_unpackhi_ps(in3, in0);
__m256 tmp6 = _mm256_unpacklo_ps(in1, in2);
__m256 tmp7 = _mm256_unpackhi_ps(in1, in2);
tmp0 = _mm256_shuffle_ps(tmp4, tmp6, 0x44);
tmp1 = _mm256_shuffle_ps(tmp6, tmp4, 0xee);
tmp2 = _mm256_shuffle_ps(tmp5, tmp7, 0x44);
tmp3 = _mm256_shuffle_ps(tmp7, tmp5, 0xee);
tout0 = _mm256_permute2f128_ps(tmp0, tmp0, 0x00);
tout1 = _mm256_permute2f128_ps(tmp1, tmp1, 0x00);
tout2 = _mm256_permute2f128_ps(tmp2, tmp2, 0x00);
tout3 = _mm256_permute2f128_ps(tmp3, tmp3, 0x00);
tout4 = _mm256_permute2f128_ps(tmp0, tmp0, 0x44);
tout5 = _mm256_permute2f128_ps(tmp1, tmp1, 0x44);
tout6 = _mm256_permute2f128_ps(tmp2, tmp2, 0x44);
out0 = tout0;
out1 = tout1;
out2 = tout2;
out3 = tout3;
out4 = tout4;
out5 = tout5;
out6 = tout6;
}
void T7x4_blend(__m256 in0, __m256 in1, __m256 in2, __m256 in3, __m256& out0, __m256& out1, __m256& out2, __m256& out3,
__m256& out4, __m256& out5, __m256& out6)
{
__m256 tout0, tout1, tout2, tout3, tout4, tout5, tout6;
__m256 tmp0, tmp1, tmp2, tmp3;
__m256 tmp4 = _mm256_unpacklo_ps(in3, in0);
__m256 tmp5 = _mm256_unpackhi_ps(in3, in0);
__m256 tmp6 = _mm256_unpacklo_ps(in1, in2);
__m256 tmp7 = _mm256_unpackhi_ps(in1, in2);
tmp0 = _mm256_shuffle_ps(tmp4, tmp6, 0x44);
tmp1 = _mm256_shuffle_ps(tmp6, tmp4, 0xee);
tmp2 = _mm256_shuffle_ps(tmp5, tmp7, 0x44);
tmp3 = _mm256_shuffle_ps(tmp7, tmp5, 0xee);
tout0 = _mm256_permute2f128_ps(tmp0, tmp0, 0x00);
tout1 = _mm256_permute2f128_ps(tmp1, tmp1, 0x00);
tout2 = _mm256_permute2f128_ps(tmp2, tmp2, 0x00);
tout3 = _mm256_permute2f128_ps(tmp3, tmp3, 0x00);
tout4 = _mm256_permute2f128_ps(tmp0, tmp0, 0x44);
tout5 = _mm256_permute2f128_ps(tmp1, tmp1, 0x44);
tout6 = _mm256_permute2f128_ps(tmp2, tmp2, 0x44);
out0 = _mm256_blend_ps(out0, tout0, 0xfe);
out1 = _mm256_blend_ps(out1, tout1, 0xfe);
out2 = _mm256_blend_ps(out2, tout2, 0xfe);
out3 = _mm256_blend_ps(out3, tout3, 0xfe);
out4 = _mm256_blend_ps(out4, tout4, 0xfe);
out5 = _mm256_blend_ps(out5, tout5, 0xfe);
out6 = _mm256_blend_ps(out6, tout6, 0xfe);
}
BENCHMARK_F(FixtureBenchmark_m256, 7x4_assign)(benchmark::State& state)
{
for (auto _ : state)
{
for (std::size_t i = 0; i < 100; ++i)
{
T7x4_assign(in[i][0], in[i][1], in[i][2], in[i][3], out[i][0], out[i][1], out[i][2], out[i][3], out[i][4],
out[i][5], out[i][6]);
benchmark::ClobberMemory();
}
}
}
BENCHMARK_F(FixtureBenchmark_m256, 7x4_blend)(benchmark::State& state)
{
for (auto _ : state)
{
for (std::size_t i = 0; i < 100; ++i)
{
T7x4_blend(in[i][0], in[i][1], in[i][2], in[i][3], out[i][0], out[i][1], out[i][2], out[i][3], out[i][4],
out[i][5], out[i][6]);
benchmark::ClobberMemory();
}
}
}
BENCHMARK_MAIN();
Это дало вывод:
---------------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------------
FixtureBenchmark_m256/7x4_assign 646 ns 646 ns 1081509
FixtureBenchmark_m256/7x4_blend 380 ns 380 ns 1847485
Проблема здесь в l oop. Я не могу точно сказать, что именно происходит, может быть, кеширование пропало или какие-то странные оптимизации l oop, но удаление l oop дает ожидаемое время:
---------------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------------
FixtureBenchmark_m256/7x4_assign 3.27 ns 3.27 ns 214698649
FixtureBenchmark_m256/7x4_blend 4.15 ns 4.14 ns 168642478
Так почему же циклы в первое место? Это произошло из-за установки google benchmark в Ubuntu с использованием sudo apt-get install libbenchmark-dev
. Проблема в том, что это отладочная сборка, и в этой версии округляются тайминги наносекунд. Таким образом, я не мог видеть никакой разницы для одного выполнения и синхронизировал несколько вызовов функций с помощью al oop. Однако после ручной сборки и установки версии выпуска я получил более точные тайминги и смог удалить l oop, что негативно сказалось на тесте.
Дополнительное замечание: я также неправильно рассчитал ожидаемые циклы ЦП. Я использовал не оптимизированную сборку, а встроенные. Итак, я придумал 8 нормальных перетасовок и 7 перестановок между рядами, которые дают 15. Добавление неизбежной задержки последней перестановки между рядами (2 дополнительных цикла) дало 17. Однако компилятор оптимизирует 3 _mm256_permute2f128_ps
, что дает 14 (12 тасует - как сказал Питер Кордес - плюс 2 цикла задержки). Деление на частоту процессора 4,2 дает 3,33, что довольно близко к результату теста.
ОБНОВЛЕНИЕ
Мне было интересно, почему компилятор оптимизировал 3 _mm256_permute2f128_ps
звонки. В моей библиотеке встроенные функции обобщены, чтобы легко поменять тип регистра. Кроме того, все маски рассчитываются автоматически. Поэтому я сделал несколько ошибок, когда заменил все вызовы библиотеки. Вот правильный код:
void Transpose7x4(__m256 in0, __m256 in1, __m256 in2, __m256 in3, __m256& out0, __m256& out1, __m256& out2,
__m256& out3, __m256& out4, __m256& out5, __m256& out6)
{
__m256 tout0, tout1, tout2, tout3, tout4, tout5, tout6;
__m256 tmp0, tmp1, tmp2, tmp3;
__m256 tmp4 = _mm256_unpacklo_ps(in3, in0);
__m256 tmp5 = _mm256_unpackhi_ps(in3, in0);
__m256 tmp6 = _mm256_unpacklo_ps(in1, in2);
__m256 tmp7 = _mm256_unpackhi_ps(in1, in2);
tmp0 = _mm256_shuffle_ps(tmp4, tmp6, 0x44);
tmp1 = _mm256_shuffle_ps(tmp4, tmp6, 0xee);
tmp2 = _mm256_shuffle_ps(tmp5, tmp7, 0x44);
tmp3 = _mm256_shuffle_ps(tmp5, tmp7, 0xee);
tout0 = _mm256_permute2f128_ps(tmp0, tmp0, 0x00);
tout1 = _mm256_permute2f128_ps(tmp1, tmp1, 0x00);
tout2 = _mm256_permute2f128_ps(tmp2, tmp2, 0x00);
tout3 = _mm256_permute2f128_ps(tmp3, tmp3, 0x00);
tout4 = _mm256_permute2f128_ps(tmp0, tmp0, 0x33);
tout5 = _mm256_permute2f128_ps(tmp1, tmp1, 0x33);
tout6 = _mm256_permute2f128_ps(tmp2, tmp2, 0x33);
out0 = tout0;
out1 = tout1;
out2 = tout2;
out3 = tout3;
out4 = tout4;
out5 = tout5;
out6 = tout6;
//out0 = _mm256_blend_ps(out0, tout0, 0xfe);
//out1 = _mm256_blend_ps(out1, tout1, 0xfe);
//out2 = _mm256_blend_ps(out2, tout2, 0xfe);
//out3 = _mm256_blend_ps(out3, tout3, 0xfe);
//out4 = _mm256_blend_ps(out4, tout4, 0xfe);
//out5 = _mm256_blend_ps(out5, tout5, 0xfe);
//out6 = _mm256_blend_ps(out6, tout6, 0xfe);
}
Теперь все инструкции (8 перемешиваний и 7 перемешиваний между рядами) появляются в сборке, как и ожидалось:
Godbolt