Изначально исследуя влияние #pragma omp simd
Директива, я столкнулся с поведением, которое я не могу объяснить, связанным с векторизацией простого цикла for. Следующий пример кода может быть протестирован на этом удивительном проводник компилятора, при условии -O3 Директива применяется, и мы находимся на архитектуре x86.
Может ли кто-нибудь объяснить мне логику следующих наблюдений?
#include <stdint.h>
void test(uint8_t* out, uint8_t const* in, uint32_t length)
{
unsigned const l1 = (length * 32)/32; // This is vectorized
unsigned const l2 = (length / 32)*32; // This is not vectorized
unsigned const l3 = (length << 5)>>5; // This is vectorized
unsigned const l4 = (length >> 5)<<5; // This is not vectorized
unsigned const l5 = length -length%32; // This is not vectorized
unsigned const l6 = length & ~(32 -1); // This is not vectorized
for (unsigned i = 0; i<l1 /*pick your choice*/; ++i)
{
out[i] = in[i*2];
}
}
Что меня озадачивает, так это то, что и l1, и l3 генерируют векторизованный код, несмотря на то, что не гарантируется, что он будет кратен 32. Все остальные длины делают не производить векторизованный код, но должен быть кратным 32. Есть ли причина этого?
Кроме того, использование директивы simp #pragma omp фактически ничего не меняет.
Редактировать: После дальнейшего исследования, разница в поведении исчезает, когда тип индекса size_t (и даже манипулирование границами не требуется), что означает, что это генерирует векторизованный код:
#include <stdint.h>
#include <string>
void test(uint8_t* out, uint8_t const* in, size_t length)
{
for (size_t i = 0; i<length; ++i)
{
out[i] = in[i*2];
}
}
Если кто-то знает, почему векторизация цикла так зависит от типа индекса, мне было бы интересно узнать больше!
Edit2, спасибо Марку Лакате, O3 действительно нужен
Проблема очевидного преобразования1 от unsigned
в size_t
в индексе массива: in[i*2];
Если вы используете l1
или же l3
тогда вычисление i*2
всегда будет соответствовать типу size_t
, Это означает, что тип unsigned
практически ведет себя так, как будто это было size_t
,
Но когда вы используете другие параметры, результат вычисления i*2
может не вписаться в size_t
поскольку значение может быть перенесено, и преобразование должно быть сделано.
если вы берете свой первый пример, не выбирая опции l1 или l3, и выполняете приведение:
out[i] = in[( size_t )i*2];
компилятор оптимизирует, если вы приведете все выражение:
out[i] = in[( size_t )(i*2)];
это не так.
1 Стандарт на самом деле не указывает, что тип в индексе должен быть size_t
, но это логичный шаг с точки зрения компилятора.
Я полагаю, вы путаете оптимизацию с векторизацией. Я использовал твой проводник компилятора и установите -O2 для x86, и ни один из примеров не «векторизован».
Вот l1
test(unsigned char*, unsigned char const*, unsigned int):
xorl %eax, %eax
andl $134217727, %edx
je .L1
.L5:
movzbl (%rsi,%rax,2), %ecx
movb %cl, (%rdi,%rax)
addq $1, %rax
cmpl %eax, %edx
ja .L5
.L1:
rep ret
Вот l2
test(unsigned char*, unsigned char const*, unsigned int):
andl $-32, %edx
je .L1
leal -1(%rdx), %eax
leaq 1(%rdi,%rax), %rcx
xorl %eax, %eax
.L4:
movl %eax, %edx
addq $1, %rdi
addl $2, %eax
movzbl (%rsi,%rdx), %edx
movb %dl, -1(%rdi)
cmpq %rcx, %rdi
jne .L4
.L1:
rep ret
Это неудивительно, потому что то, что вы делаете, — это, по сути, операция загрузки «сбор», где индексы загрузки не совпадают с индексами хранилища. В x86 нет поддержки для сбора / разброса. Это только введено в AVX2 и AVX512, и это не выбрано.
Немного более длинный код имеет дело с проблемами со знаком / без знака, но векторизация не происходит.