Вы можете увидеть влияние кода на использование GNU C / C ++ __builtin_assume_aligned
.
gcc 7 и более ранних версий для x86 (и ICC18), предпочитающих использовать скалярный прологчтобы достичь границы выравнивания, затем выровненный векторный цикл, затем скалярный эпилог для очистки любых оставшихся элементов, которые не были кратны полному вектору.
Рассмотрим случай, когда общее числоВо время компиляции элементы известны как кратные ширине вектора, но выравнивание неизвестно. Если вы знали выравнивание, вам не нужны ни пролог, ни эпилог.Но если нет, вам нужны оба. Число оставшихся элементов после последнего выровненного вектора неизвестно.
Эта ссылка проводника компилятора Godbolt показывает эти функции, скомпилированные дляx86-64 с ICC18, gcc7.3 и clang6.0.clang очень агрессивно развертывает , но все еще использует неприсоединившиеся магазины.Это выглядит как странный способ потратить столько кода на цикл, который просто хранит.
// aligned, and size a multiple of vector width
void set42_aligned(int *p) {
p = (int*)__builtin_assume_aligned(p, 64);
for (int i=0 ; i<1024 ; i++ ) {
*p++ = 0x42;
}
}
# gcc7.3 -O3 (arch=tune=generic for x86-64 System V: p in RDI)
lea rax, [rdi+4096] # end pointer
movdqa xmm0, XMMWORD PTR .LC0[rip] # set1_epi32(0x42)
.L2: # do {
add rdi, 16
movaps XMMWORD PTR [rdi-16], xmm0
cmp rax, rdi
jne .L2 # }while(p != endp);
rep ret
Это почти то же самое, что я бы сделал вручную, за исключением, может быть,Развертывание на 2, чтобы OoO exec мог обнаружить, что ветвь выхода из цикла не была занята, пока он жует магазины.
Таким образом, версия без выравнивания включает пролог и эпилог:
// without any alignment guarantee
void set42(int *p) {
for (int i=0 ; i<1024 ; i++ ) {
*p++ = 0x42;
}
}
~26 instructions of setup, vs. 2 from the aligned version
.L8: # then a bloated loop with 4 uops instead of 3
add eax, 1
add rdx, 16
movaps XMMWORD PTR [rdx-16], xmm0
cmp ecx, eax
ja .L8 # end of main vector loop
# epilogue:
mov eax, esi # then destroy the counter we spent an extra uop on inside the loop. /facepalm
and eax, -4
mov edx, eax
sub r8d, eax
cmp esi, eax
lea rdx, [r9+rdx*4] # recalc a pointer to the last element, maybe to avoid a data dependency on the pointer from the loop.
je .L5
cmp r8d, 1
mov DWORD PTR [rdx], 66 # fully-unrolled final up-to-3 stores
je .L5
cmp r8d, 2
mov DWORD PTR [rdx+4], 66
je .L5
mov DWORD PTR [rdx+8], 66
.L5:
rep ret
Даже для более сложного цикла, который выиграл бы от небольшого развертывания, gcc оставляет основной векторизованный цикл вообще не развернутым, но затрачивает множество кода размером на полностью развернутый скалярный пролог / эпилог.Это действительно плохо для AVX2 256-битной векторизации с uint16_t
элементами или чем-то еще.(до 15 элементов в прологе / эпилоге, а не 3).Это не разумный компромисс, поэтому он помогает gcc7 и более ранним версиям значительно определить, когда указатели выровнены.(Скорость выполнения не сильно меняется, но это существенно влияет на уменьшение количества кода.)
Кстати, gcc8 предпочитает использовать невыровненные загрузки / хранилища при условии, что данные часто выровнены,Современное оборудование имеет дешевые невыровненные 16- и 32-байтовые загрузки / хранилища, поэтому позволить аппаратному обеспечению справиться со стоимостью загрузок / хранилищ, которые разбиты по границе строки кэша, часто хорошо.(AVX512 64-байтовые хранилища часто стоят того, чтобы их выровнять, потому что любое смещение означает разделение строки кэша на каждый доступ, а не на каждый другой или каждый 4-й.)
Еще один фактор - ранние gccполностью развернутые скалярные прологи / эпилоги являются дерьмом по сравнению с умной обработкой, когда вы делаете один невыровненный потенциально перекрывающийся вектор в начале / конце.( См. Эпилог в этой рукописной версии set42
).Если бы gcc знал, как это сделать, стоило бы выравнивать его чаще.