У меня есть ряд жестких циклов, которые я пытаюсь оптимизировать с помощью GCC и intrinsics. Рассмотрим, например, следующую функцию.
void triad(float *x, float *y, float *z, const int n) {
float k = 3.14159f;
int i;
__m256 k4 = _mm256_set1_ps(k);
for(i=0; i<n; i+=8) {
_mm256_store_ps(&z[i], _mm256_add_ps(_mm256_load_ps(&x[i]), _mm256_mul_ps(k4, _mm256_load_ps(&y[i]))));
}
}
Это создает основной цикл, подобный этому
20: vmulps ymm0,ymm1,[rsi+rax*1]
25: vaddps ymm0,ymm0,[rdi+rax*1]
2a: vmovaps [rdx+rax*1],ymm0
2f: add rax,0x20
33: cmp rax,rcx
36: jne 20
Но инструкция cmp
не нужна. Вместо того, чтобы rax
начинать с нуля и заканчивать в sizeof(float)*n
, мы можем установить базовые указатели (rsi
, rdi
и rdx
) в конец массива и установить rax
в -sizeof(float)*n
а затем проверить на нуль. Я могу сделать это с помощью моего собственного кода сборки, подобного этому
.L2 vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm0, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm0
add rax, 32
jne .L2
но я не могу заставить GCC сделать это. Сейчас у меня несколько тестов, где это имеет существенное значение. До недавнего времени GCC и intrinsics меня очень сильно ранили, поэтому мне интересно, есть ли компилятор или способ изменить порядок/изменить код, поэтому команда cmp
не создается с помощью GCC.
Я попробовал следующее, но он все еще производит cmp
. Все варианты, которые я попытался, все еще создают cmp
.
void triad2(float *x, float *y, float *z, const int n) {
float k = 3.14159f;
float *x2 = x+n;
float *y2 = y+n;
float *z2 = z+n;
int i;
__m256 k4 = _mm256_set1_ps(k);
for(i=-n; i<0; i+=8) {
_mm256_store_ps(&z2[i], _mm256_add_ps(_mm256_load_ps(&x2[i]), _mm256_mul_ps(k4, _mm256_load_ps(&y2[i]))));
}
}
Изменить:
Меня интересует максимизация уровня команд parallelism (ILP) для этих функций для массивов, которые вписываются в кеш L1 (фактически для n=2048
). Хотя разворачивание может использоваться для улучшения полосы пропускания, оно может уменьшить ILP (при условии, что полная пропускная способность может быть достигнута без разворота).
Изменить:
Вот таблица результатов для Core2 (pre Nehalem), IvyBridge и системы Haswell. Intrinsics - это результат использования intrinsics, unroll1 - мой код сборки, не использующий cmp
, а unroll16 - мой код сборки, разворачивающий 16 раз. Процентное соотношение - это процентная доля максимальной производительности (частота * num_bytes_cycle, где num_bytes_cycle - 24 для SSE, 48 для AVX и 96 для FMA).
SSE AVX FMA
intrinsic 71.3% 90.9% 53.6%
unroll1 97.0% 96.1% 63.5%
unroll16 98.6% 90.4% 93.6%
ScottD 96.5%
32B code align 95.5%
Для SSE я получаю почти такой же результат без разворачивания, как при разворачивании, но только если я не использую cmp
. В AVX я получаю лучший результат без разворачивания и без использования cmp
. Интересно, что на IB разворачивание на самом деле хуже. На Хасуэле я получаю лучший результат при разворачивании. Вот почему я спросил об этом question. Исходный код для проверки этого может быть найден в этом вопросе.
Edit:
Основываясь на ответе ScottD, теперь я получаю почти 97% с внутренними функциями моей системы Core2 (64-разрядный режим до Nehalem). Я не уверен, почему вопрос cmp
имеет значение, поскольку он должен так или иначе, за два такта. Для Sandy Bridge получается, что потеря эффективности связана с выравниванием кода не с дополнительным cmp
. В любом случае Haswell работает только разворачивание.