Почему простой цикл оптимизирован, когда предел равен 959, но не 960?
Рассмотрим этот простой цикл:
float f(float x[]) {
float p = 1.0;
for (int i = 0; i < 959; i++)
p += 1;
return p;
}
Если вы скомпилируете gcc 7 (snapshot) или clang (trunk) с помощью -march=core-avx2 -Ofast
, вы получите что-то очень похожее.
.LCPI0_0:
.long 1148190720 # float 960
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
ret
Другими словами, он просто устанавливает ответ на 960 без цикла.
Однако, если вы измените код на:
float f(float x[]) {
float p = 1.0;
for (int i = 0; i < 960; i++)
p += 1;
return p;
}
Произведенная сборка фактически выполняет сумму цикла? Например, clang дает:
.LCPI0_0:
.long 1065353216 # float 1
.LCPI0_1:
.long 1086324736 # float 6
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
vxorps ymm1, ymm1, ymm1
mov eax, 960
vbroadcastss ymm2, dword ptr [rip + .LCPI0_1]
vxorps ymm3, ymm3, ymm3
vxorps ymm4, ymm4, ymm4
.LBB0_1: # =>This Inner Loop Header: Depth=1
vaddps ymm0, ymm0, ymm2
vaddps ymm1, ymm1, ymm2
vaddps ymm3, ymm3, ymm2
vaddps ymm4, ymm4, ymm2
add eax, -192
jne .LBB0_1
vaddps ymm0, ymm1, ymm0
vaddps ymm0, ymm3, ymm0
vaddps ymm0, ymm4, ymm0
vextractf128 xmm1, ymm0, 1
vaddps ymm0, ymm0, ymm1
vpermilpd xmm1, xmm0, 1 # xmm1 = xmm0[1,0]
vaddps ymm0, ymm0, ymm1
vhaddps ymm0, ymm0, ymm0
vzeroupper
ret
Почему это и почему это точно так же для clang и gcc?
Предел для одного и того же цикла, если вы замените float
на double
равным 479. Это то же самое для gcc и clang.
Обновление 1
Оказывается, что gcc 7 (snapshot) и clang (trunk) ведут себя по-разному. Насколько я могу судить, clang оптимизирует петли для всех пределов меньше 960. gcc, с другой стороны, чувствителен к точному значению и не имеет верхнего предела. Например, не оптимизирует цикл, когда предел равен 200 (а также многие другие значения), но делает, когда предел равен 202 и 20002 (а также многие другие значения).
Ответы
Ответ 1
TL; DR
По умолчанию текущий снимок GCC 7 ведет себя непоследовательно, а предыдущие версии имеют ограничение по умолчанию из-за PARAM_MAX_COMPLETELY_PEEL_TIMES
, что равно 16. Его можно переопределить из командной строки.
Обоснование ограничения - предотвращать слишком агрессивную петлю, которая может быть обоюдоострым мечом.
Версия GCC <= 6.3.0
Соответствующая опция оптимизации для GCC -fpeel-loops
, которая активируется косвенно вместе с флагом -Ofast
(акцент мой):
Пилинг-петли, для которых достаточно информации о том, что они не (от обратной связи профиля или статического анализа ). Он также включается полный пилинг (т.е. полное удаление петель с небольшим постоянное количество итераций).
Включено с помощью -O3
и/или -fprofile-use
.
Более подробную информацию можно получить, добавив -fdump-tree-cunroll
:
$ head test.c.151t.cunroll
;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)
Not peeling: upper bound is known so can unroll completely
Сообщение от /gcc/tree-ssa-loop-ivcanon.c
:
if (maxiter >= 0 && maxiter <= npeel)
{
if (dump_file)
fprintf (dump_file, "Not peeling: upper bound is known so can "
"unroll completely\n");
return false;
}
следовательно try_peel_loop
возвращает false
.
Более подробный вывод может быть достигнут с помощью -fdump-tree-cunroll-details
:
Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely
Можно настроить пределы с помощью max-completely-peeled-insns=n
и max-completely-peel-times=n
params:
max-completely-peeled-insns
Максимальное количество insns полностью очищенного контура.
max-completely-peel-times
Максимальное количество итераций цикла, подходящих для полного шелушение.
Чтобы узнать больше о insns, вы можете обратиться к Руководство по внутренним документам GCC.
Например, если вы компилируете следующие параметры:
-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000
тогда код превращается в:
f:
vmovss xmm0, DWORD PTR .LC0[rip]
ret
.LC0:
.long 1148207104
Clang
Я не уверен, что делает Clang на самом деле, и как настроить его пределы, но, как я заметил, вы можете заставить его оценить окончательное значение, отметив цикл unroll pragma, и он полностью удалит его:
#pragma unroll
for (int i = 0; i < 960; i++)
p++;
результат:
.LCPI0_0:
.long 1148207104 # float 961
f: # @f
vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
ret
Ответ 2
После прочтения комментария Sulthan, я предполагаю, что:
-
Компилятор полностью разворачивает цикл, если счетчик цикла является постоянным (и не слишком высоким)
-
Как только он развернут, компилятор видит, что операции суммирования могут быть сгруппированы в один.
Если цикл по какой-то причине не разворачивается (здесь: он генерирует слишком много операторов с 1000
), операции не могут быть сгруппированы.
Компилятор мог видеть, что разворот 1000 операторов составляет одно добавление, но описанные выше шаги 1 и 2 представляют собой две отдельные оптимизации, поэтому он не может принять "риск" разворачивания, не зная, могут ли операции сгруппироваться (пример: вызов функции не может быть сгруппирован).
Примечание. Это угловой случай: кто использует цикл, чтобы снова добавить одно и то же? В этом случае не полагайтесь на компилятор, который можно развернуть/оптимизировать; непосредственно пишите правильную операцию в одной инструкции.
Ответ 3
Очень хороший вопрос!
Похоже, вы достигли предела в количестве итераций или операций, которые компилятор пытается встроить при упрощении кода. Как описано в Grzegorz Szpetkowski, существуют специфические способы компиляции для настройки этих ограничений с помощью параметров pragmas или командной строки.
Вы также можете играть с Godbolt Compiler Explorer, чтобы сравнить, как разные компиляторы и параметры влияют на генерируемый код: gcc 6.2
и icc 17
все еще встроить код для 960, тогда как clang 3.9
не имеет (с конфигурацией Godbolt по умолчанию, он фактически прекращает вложение в 73).