Почему начальная загрузка векторизации из выровненного std :: array является скалярной? (Г ++ / лязг ++)

У меня проблемы с пониманием того, что мешает компиляторам использовать начальные векторные нагрузки при чтении данных из станд :: массив<uint64_t, …>.

Я знаю, что gcc может выдавать отладочную информацию с помощью -fopt-info-vec- *. В подробном журнале я не могу найти ничего, что указывало бы, почему оба компилятора принимают одно и то же неоптимальное решение использовать начальные скалярные нагрузки.

С другой стороны, я не знаю, как сделать Clang, чтобы предоставить подробную информацию о проблемах векторизации. -Rpass-analysis = loop-vectorize только сообщает, что цикл в init не стоит чередовать. Конечно, моя внутренняя версия является доказательством того, что цикл может быть векторизованным, но требуемые преобразования, вероятно, слишком сложны, кроме как из компилятора.

Конечно, я мог бы реализовать горячие пути с использованием встроенных функций, но это требует дублирования одной и той же логики для каждой архитектуры процессора. Я бы предпочел написать стандартный код C ++, который компилятор может векторизовать почти идеально. Становится просто скомпилировать один и тот же код несколько раз с разными флагами, используя атрибут target_clones или макросы и целевой атрибут.

Как заставить компилятор сказать, почему нагрузки не удалось векторизовать?

Я подозреваю, что gcc может уже напечатать ту информацию, которую я просто не знаю, что я ищу.

Почему автоматическая векторизация дает сбой при начальной загрузке?

    /**
* This is a test case removing abstraction layers from my actual code. My
* real code includes one extra problem that access to pack loses alignment
* information wasn't only issue. Compilers still generate
* suboptimal machine code with alignment information present. I fail to
* understand why loads are treated differently compared to stores to
* same address when auto-vectorization is used.
*
* I tested gcc 6.2 and clang 3.9
* g++ O3 -g -march=native vectest.cc -o vectest -fvect-cost-model=unlimited
* clang++ -O3 -g -march=native vectest.cc -o vectest
*/#include <array>
#include <cstdint>

alignas(32) std::array<uint64_t, 52> pack;
alignas(32) uint64_t board[4];

__attribute__((noinline))
static void init(uint64_t initial)
{
/* Clang seem to prefer large constant table and unrolled copy
* which should perform worse outside micro benchmark. L1 misses
* and memory bandwidth are bigger bottleneck than alu instruction
* execution. But of course this code won't be compiled to hot path so
* I don't care how it is compiled as long as it works correctly.
*
* But most interesting detail from clang is vectorized stores are
* generated correctly like:
4005db:       vpsllvq %ymm2,%ymm1,%ymm2
4005e0:       vmovdqa %ymm2,0x200a78(%rip)        # 601060 <pack>
4005e8:       vpaddq 0x390(%rip),%ymm0,%ymm2        # 400980 <_IO_stdin_used+0x60>
4005f0:       vpsllvq %ymm2,%ymm1,%ymm2
4005f5:       vmovdqa %ymm2,0x200a83(%rip)        # 601080 <pack+0x20>
4005fd:       vpaddq 0x39b(%rip),%ymm0,%ymm2        # 4009a0 <_IO_stdin_used+0x80>
*
* gcc prefers scalar loop.
*/

for (unsigned i = 0; i < pack.size(); i++) {
pack[i] = 1UL << (i + initial);
}
}

#include "immintrin.h"__attribute__((noinline))
static void expected_init(uint64_t initial)
{
/** Just an intrinsic implementation of init that would be IMO ideal
* optimization.
*/
#if __AVX2__
unsigned i;
union {
uint64_t *mem;
__m256i *avx;
} conv;
conv.mem = &pack[0];
__m256i t = _mm256_set_epi64x(
1UL << 3,
1UL << 2,
1UL << 1,
1UL << 0
);
/* initial is just extra random number to prevent constant array
* initialization
*/
t = _mm256_slli_epi64(t, initial);
for(i = 0; i < pack.size()/4; i++) {
_mm256_store_si256(&conv.avx[i], t);
t = _mm256_slli_epi64(t, 4);
}
#endif
}

__attribute__((noinline))
static void iter_or()
{
/** initial load (clang):
4006f0:       vmovaps 0x200988(%rip),%xmm0        # 601080 <pack+0x20>
4006f8:       vorps  0x200960(%rip),%xmm0,%xmm0        # 601060 <pack>
400700:       vmovaps 0x200988(%rip),%xmm1        # 601090 <pack+0x30>
400708:       vorps  0x200960(%rip),%xmm1,%xmm1        # 601070 <pack+0x10>
400710:       vinsertf128 $0x1,%xmm1,%ymm0,%ymm0
* expected:
400810:       vmovaps 0x200868(%rip),%ymm0        # 601080 <pack+0x20>
400818:       vorps  0x200840(%rip),%ymm0,%ymm0        # 601060 <pack>
400820:       vorps  0x200878(%rip),%ymm0,%ymm0        # 6010a0 <pack+0x40>
*/

auto iter = pack.begin();
uint64_t n(*iter++),
e(*iter++),
s(*iter++),
w(*iter++);
for (;iter != pack.end();) {
n |= *iter++;
e |= *iter++;
s |= *iter++;
w |= *iter++;
}
/** Store is correctly vectorized to single instruction */
board[0] = n;
board[1] = e;
board[2] = s;
board[3] = w;
}

__attribute__((noinline))
static void index_or()
{
/** Clang compiles this to same as iterator variant. gcc goes
* completely insane. I don't even want to try to guess what all the
* permutation stuff is trying to archive.
*/
unsigned i;
uint64_t n(pack[0]),
e(pack[1]),
s(pack[2]),
w(pack[3]);
for (i = 4 ; i < pack.size(); i+=4) {
n |= pack[i+0];
e |= pack[i+1];
s |= pack[i+2];
w |= pack[i+3];
}
board[0] = n;
board[1] = e;
board[2] = s;
board[3] = w;
}

#include "immintrin.h"
__attribute__((noinline))
static void expected_result()
{
/** Intrinsics implementation what I would expect auto-vectorization
* transform my c++ code. I simple can't understand why both compilers
* fails to archive results I expect.
*/
#if __AVX2__
union {
uint64_t *mem;
__m256i *avx;
} conv;
conv.mem = &pack[0];
unsigned i;
__m256i res = _mm256_load_si256(&conv.avx[0]);
for (i = 1; i < pack.size()/4; i++) {
__m256i temp = _mm256_load_si256(&conv.avx[i]);
res = _mm256_or_si256(res, temp);
}
conv.mem = board;
_mm256_store_si256(conv.avx, res);
#endif
}

int main(int c, char **v)
{
(void)v;
expected_init(c - 1);
init(c - 1);

iter_or();
index_or();
expected_result();
}

1

Решение

Похоже, что gcc и clang не могут векторизовать начальную загрузку извне цикла. Если сначала изменить код на нулевые временные переменные, а затем использовать или из первого элемента, то оба компилятора работают лучше. Clang генерирует хороший развернутый векторный код (узким местом являются только одиночные регистры ymm со всеми инструкциями, имеющими зависимость от предыдущего). GCC генерирует немного худший код с дополнительным начальным vpxor и довольно плохим циклом, выполняющим один vpor за итерацию.

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

/* only reduce (calling this function from a for loop):
* ST 7.3 cycles (ST=single thread)
* SMT 15.3 cycles (SMT=simultaneous multi threading aka hyper threading)
* shuffle+reduce (calling Fisher-Yatas shuffle and then this function):
* ST 222 cycles
* SMT 383 cycles
*/
"vmovaps 0x00(%0), %%ymm0\n""vmovaps 0x20(%0), %%ymm1\n""vpor 0x40(%0), %%ymm0, %%ymm0\n""vpor 0x60(%0), %%ymm1, %%ymm1\n""vpor 0x80(%0), %%ymm0, %%ymm0\n""vpor 0xA0(%0), %%ymm1, %%ymm1\n""vpor 0xC0(%0), %%ymm0, %%ymm0\n""vpor 0xE0(%0), %%ymm1, %%ymm1\n""vpor 0x100(%0), %%ymm0, %%ymm0\n""vpor 0x120(%0), %%ymm1, %%ymm1\n""vpor 0x140(%0), %%ymm0, %%ymm0\n""vpor 0x160(%0), %%ymm1, %%ymm1\n""vpor 0x180(%0), %%ymm0, %%ymm0\n"
"vpor %%ymm0, %%ymm1, %%ymm0\n""vmovaps %%ymm0, 0x00(%1)\n"

У развернутого цикла Clang есть такие моменты, как

/* only reduce:
* ST 9.8 cycles
* SMT 21.8 cycles
* shuffle+reduce:
* ST 223 cycles
* SMT 385 cycles
*/

Но цифры, в которых SMT снижала производительность развернутого кода, выглядели подозрительно. Я решил попробовать лучше написать цикл GCC, который был явно медленнее, чем развернутый. Но затем я решил разорвать зависимости инструкций, используя два регистра и один раз развернуть цикл. Это привело к несколько более быстрому перемешиванию + уменьшению кода, чем полное развертывание.

size_t end = pack.size() - 3*4;
asm (
/* The best SMT option outside micro optimization.
* This allows executing two vpor instructions same time and
* reduces loop count to half with single unroll
*
* only reduce:
* ST 13.0 cycles
* SMT 20.0 cycles
* shuffle+reduce:
* ST 221 cycles
* SMT 380 cycles
*/
"vmovaps 0x180(%[pack]), %%ymm0\n""vmovaps 0x160(%[pack]), %%ymm1\n""vpor 0x00(%[pack],%[cnt],8), %%ymm0, %%ymm0\n""1:\n""vpor -0x20(%[pack],%[cnt],8), %%ymm1, %%ymm1\n""vpor -0x40(%[pack],%[cnt],8), %%ymm0, %%ymm0\n""sub $8, %[cnt]\n""jne 1b\n"
"vpor %%ymm0, %%ymm1, %%ymm0\n""vmovaps %%ymm0, 0x00(%[out])\n": [cnt]"+r"(end)
: [pack]"r"(begin), [out]"r"(hands_));

Но различия удивительно малы, когда код запускается после перемешивания Фишера-Йейтса. Даже версия gcc с явным проигрышем в тесте «только уменьшение» (16.4 / 38.8) запускает случайное перемешивание + тестирование при приближении к одинаковой скорости (228/387)

1

Другие решения

Других решений пока нет …