Почему простой цикл оптимизирован, когда предел равен 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).