Рассмотрим этот простой цикл:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

Если вы скомпилируете с помощью gcc 7 (снимок) или 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 (снимок) и clang (ствол) ведут себя по-разному. clang оптимизирует циклы для всех лимитов меньше 960, насколько я могу судить. gcc, с другой стороны, чувствителен к точному значению и не имеет верхнего предела. Например, не оптимизирует цикл, когда предел равен 200 (как и многие другие значения), но делает, когда предел равен 202 и 20002 (а также многие другие значения).

graffe

Ответов: 3

Ответы (3)

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-complete-peeled-insns = n и max-complete-peel-times = n params:

макс. Полностью очищенные вставки

Максимальное количество инснов полностью очищенной петли.

максимальное время полного отслаивания

Максимальное количество итераций цикла, подходящее для завершения пилинг.

Чтобы узнать больше о insns, вы можете обратиться к GCC Internals Manual.

Например, если вы компилируете со следующими параметрами:

-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 и как настраивать его пределы, но, как я заметил, вы можете заставить его оценить окончательное значение, пометив цикл директивой развертки, и он удалит это полностью:

#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

Очень хороший вопрос!

Кажется, вы достигли предела количества итераций или операций, которые компилятор пытается встроить при упрощении кода. Как задокументировано Гжегожем Шпетковски, существуют специфические для компилятора способы настройки этих ограничений с помощью прагм или параметров командной строки.

Вы также можете поиграть с Godbolt's Compiler Explorer, чтобы сравнить, как разные компиляторы и параметры влияют на сгенерированный код: gcc 6.2 и icc 17 все еще встроен в код для 960, тогда как clang 3.9 не работает (с конфигурацией Godbolt по умолчанию он фактически прекращает встраивание на 73).

Прочитав комментарий Султана, я думаю, что:

  1. Компилятор полностью разворачивает цикл, если счетчик цикла постоянный (и не слишком высокий)

  2. После развертывания компилятор видит, что операции суммирования могут быть сгруппированы в одну.

Если по какой-то причине цикл не развернут (здесь: он генерирует слишком много операторов с 1000), операции не могут быть сгруппированы.

Компилятор может увидеть, что развертывание 1000 операторов составляет одно добавление, но шаги 1 и 2, описанные выше, представляют собой две отдельные оптимизации, поэтому он не может брать на себя «риск» развертывания, не зная если операции можно сгруппировать (пример: вызов функции не может быть сгруппирован).

Примечание. Это угловой случай: кто использует цикл, чтобы снова добавить одно и то же? В этом случае не полагайтесь на возможную развертку / оптимизацию компилятора; прямо напишите правильную операцию в одной инструкции.

2022 WebDevInsider