Есть ли способ указать GCC (я использую 4.8.4) развернуть цикл while
в нижней функции полностью, то есть очистить этот цикл? Количество итераций цикла известно во время компиляции: 58.
Позвольте мне сначала объяснить, что я пробовал.
Проверяя выход ГАЗА:
gcc -fpic -O2 -S GEPDOT.c
Используются 12 регистров XMM0 - XMM11. Если я передам флаг -funroll-loops
в gcc:
gcc -fpic -O2 -funroll-loops -S GEPDOT.c
цикл разворачивается только два раза. Я проверил параметры оптимизации GCC. GCC сообщает, что -funroll-loops
также включит -frename-registers
, поэтому, когда GCC разворачивает цикл, его предварительный выбор для распределения регистров - использовать оставшиеся регистры. Но осталось только 4 XMM12 - XMM15, поэтому GCC может развернуться только 2 раза в лучшем случае. Если бы было 48 вместо 16 доступных регистров XMM, GCC без проблем развернул бы цикл while 4 раза.
Но я провел еще один эксперимент. Сначала я дважды развернул цикл while вручную, получив функцию GEPDOT_2
. Тогда нет никакой разницы между
gcc -fpic -O2 -S GEPDOT_2.c
а также
gcc -fpic -O2 -funroll-loops -S GEPDOT_2.c
Поскольку GEPDOT_2
уже израсходовали все регистры, развертывание не выполняется.
GCC регистрирует переименование, чтобы избежать появления потенциальной ложной зависимости. Но я точно знаю, что в моем GEPDOT
такого потенциала не будет; даже если есть, это не важно. Я сам пробовал развернуть петлю, и развертка в 4 раза быстрее, чем развертка в 2 раза, быстрее, чем без развертывания. Конечно, я могу вручную развернуть еще несколько раз, но это утомительно. Может ли GCC сделать это за меня? Спасибо.
// C file "GEPDOT.c"
#include <emmintrin.h>
void GEPDOT (double *A, double *B, double *C) {
__m128d A1_vec = _mm_load_pd(A); A += 2;
__m128d B_vec = _mm_load1_pd(B); B++;
__m128d C1_vec = A1_vec * B_vec;
__m128d A2_vec = _mm_load_pd(A); A += 2;
__m128d C2_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C3_vec = A1_vec * B_vec;
__m128d C4_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C5_vec = A1_vec * B_vec;
__m128d C6_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C7_vec = A1_vec * B_vec;
A1_vec = _mm_load_pd(A); A += 2;
__m128d C8_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
int k = 58;
/* can compiler unroll the loop completely (i.e., peel this loop)? */
while (k--) {
C1_vec += A1_vec * B_vec;
A2_vec = _mm_load_pd(A); A += 2;
C2_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C3_vec += A1_vec * B_vec;
C4_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C5_vec += A1_vec * B_vec;
C6_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C7_vec += A1_vec * B_vec;
A1_vec = _mm_load_pd(A); A += 2;
C8_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
}
C1_vec += A1_vec * B_vec;
A2_vec = _mm_load_pd(A);
C2_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C3_vec += A1_vec * B_vec;
C4_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C5_vec += A1_vec * B_vec;
C6_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B);
C7_vec += A1_vec * B_vec;
C8_vec += A2_vec * B_vec;
/* [write-back] */
A1_vec = _mm_load_pd(C); C1_vec = A1_vec - C1_vec;
A2_vec = _mm_load_pd(C + 2); C2_vec = A2_vec - C2_vec;
A1_vec = _mm_load_pd(C + 4); C3_vec = A1_vec - C3_vec;
A2_vec = _mm_load_pd(C + 6); C4_vec = A2_vec - C4_vec;
A1_vec = _mm_load_pd(C + 8); C5_vec = A1_vec - C5_vec;
A2_vec = _mm_load_pd(C + 10); C6_vec = A2_vec - C6_vec;
A1_vec = _mm_load_pd(C + 12); C7_vec = A1_vec - C7_vec;
A2_vec = _mm_load_pd(C + 14); C8_vec = A2_vec - C8_vec;
_mm_store_pd(C,C1_vec); _mm_store_pd(C + 2,C2_vec);
_mm_store_pd(C + 4,C3_vec); _mm_store_pd(C + 6,C4_vec);
_mm_store_pd(C + 8,C5_vec); _mm_store_pd(C + 10,C6_vec);
_mm_store_pd(C + 12,C7_vec); _mm_store_pd(C + 14,C8_vec);
}
обновление 1
Благодаря комментарию @ user3386109 я хотел бы немного расширить этот вопрос. @ user3386109 вызывает очень хороший вопрос. На самом деле у меня есть некоторые сомнения относительно способности компилятора оптимального распределения регистров, когда нужно запланировать так много параллельных инструкций.
Я лично считаю, что надежный способ - сначала закодировать тело цикла (которое является ключом к HPC) во встроенной сборке asm, а затем дублировать его столько раз, сколько я захочу. Ранее в этом году у меня был непопулярный пост: встроенная сборка < / а>. Код был немного другим, потому что количество итераций цикла j является аргументом функции, поэтому неизвестно во время компиляции. В этом случае я не могу полностью развернуть цикл, поэтому я только дважды продублировал код сборки и преобразовал цикл в метку и прыжок. Оказалось, что результирующая производительность моей написанной сборки примерно на 5% выше, чем производительность сборки, созданной компилятором, что может указывать на то, что компилятор не может выделить регистры ожидаемым и оптимальным образом.
Я был (и остаюсь) младенцем в программировании на ассемблере, так что это хороший пример для меня, чтобы немного узнать о ассемблере x86. Но в конечном итоге я не склонен кодировать GEPDOT
с большой долей для сборки. Основных причин три:
- Встроенная сборка asm подверглась критике за непереносимость. Хотя не понимаю почему. Может из-за того, что на разных машинах засорены разные регистры?
- Компилятор тоже становится лучше. Так что я бы по-прежнему предпочел алгоритмическую оптимизацию и лучшую привычку кодирования C, чтобы помочь компилятору генерировать хороший результат;
- Последняя причина более важна. Количество итераций не всегда может быть 58. Я разрабатываю высокопроизводительную подпрограмму факторизации матрицы. Для коэффициента блокировки кеша
nb
количество итераций будетnb-2
. Я не собираюсь использоватьnb
в качестве аргумента функции, как это было в предыдущем посте. Это машинно-зависимый параметр, который будет определен как макрос. Таким образом, количество итераций известно во время компиляции, но может варьироваться от машины к машине. Угадайте, сколько утомительной работы мне нужно проделать при ручном развертывании цикла для множестваnb
. Так что, если есть способ просто указать компилятору отсоединить цикл, это прекрасно.
Буду очень признателен, если вы также поделитесь опытом создания высокопроизводительной, но переносимой библиотеки.
-funroll-all-loops
? - person Nate Eldredge   schedule 20.03.2016while
наpreproc__repeat(58)
. Затем напишите препроцессор, который ищетpreproc__repeat
, извлекает число и дублирует тело указанное количество раз. - person user3386109   schedule 20.03.2016