Заставить/убедить/обмануть GCC, чтобы развернуть _длинные_ циклы?

Как мне убедить GCC развернуть цикл, в котором количество итераций известно, но велико?

Я компилирую с -O3.

Реальный рассматриваемый код, конечно, сложнее, но вот упрощенный пример, который имеет такое же поведение:

int const constants[] = { 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144 };

int get_sum_1()
{
    int total = 0;
    for (int i = 0; i < CONSTANT_COUNT; ++i)
    {
        total += constants[i];
    }
    return total;
}

... если CONSTANT_COUNT определено как 8 (или меньше), то GCC развернет цикл, распространит константы и уменьшит всю функцию до простого return <value>;. Если, с другой стороны, CONSTANT_COUNT равно 9 (или больше), то цикл не разворачивается, и GCC создает двоичный файл, который зацикливается, считывает константы и добавляет их во время выполнения, хотя теоретически функция может по-прежнему оптимизировать до простого возврата константы. (Да, я просмотрел декомпилированный бинарный файл.)

Если я вручную разворачиваю цикл, например так:

int get_sum_2()
{
    int total = 0;
    total += constants[0];
    total += constants[1];
    total += constants[2];
    total += constants[3];
    total += constants[4];
    total += constants[5];
    total += constants[6];
    total += constants[7];
    total += constants[8];
    //total += constants[9];
    return total;
}

Или это:

#define ADD_CONSTANT(z, v, c) total += constants[v];

int get_sum_2()
{
    int total = 0;
    BOOST_PP_REPEAT(CONSTANT_COUNT, ADD_CONSTANT, _)
    return total;
}

... затем функция оптимизируется до возврата константы. Таким образом, GCC, по-видимому, может обрабатывать постоянное распространение для больших циклов после их развертывания; зависание, похоже, просто заставляет GCC рассмотреть возможность развертывания более длинного цикла в первую очередь.

Однако ни развертывание вручную, ни BOOST_PP_REPEAT не являются приемлемыми вариантами, поскольку в некоторых случаях CONSTANT_COUNT является выражением времени выполнения, а тот же код по-прежнему должен работать правильно для те случаи. (В этих случаях производительность не так критична.)

Я работаю на C (не на C++), поэтому ни метапрограммирование шаблонов, ни constexpr мне недоступны.

Я пробовал -funroll-loops, -funroll-all-loops, -fpeel-loops и устанавливал большие значения для max-unrolled-insns, max-average-unrolled-insns, max-unroll-times, max-peeled-insns, max-peel-times, max-completely-peeled-insns и max-completely-peel-times, ни одно из которых, похоже, не имеет значения.

Я использую GCC 4.8.2 в Linux, x86_64.

Любые идеи? Есть ли флаг или параметр, который мне не хватает...?


person jgustafson    schedule 16.09.2014    source источник
comment
Итак, вы в основном хотите, чтобы GCC выяснил, что будет вычислять цикл, и просто использовал это число; вас действительно не волнует, мысленно разворачивает цикл или нет, пока он сводит его к константе, верно?   -  person SamB    schedule 21.11.2015


Ответы (2)


Я не уверен, применим ли этот обходной путь к вашей реальной проблеме, но я обнаружил, что GCC 4.9.0 20140604 (предварительная версия) на x86_64 под управлением Parabola GNU/Linux разворачивает следующий цикл до CONSTANT_COUNT == 33 включительно.

int
get_sum()
{
  int total = 0;
  int i, j, k = 0;
  for (j = 0; j < 2; ++j)
    {
      for (i = 0; i < CONSTANT_COUNT / 2; ++i)
        {
          total += constants[k++];
        }
    }
  if (CONSTANT_COUNT % 2)
    total += constants[k];
  return total;
}

Я только передал ему флаг -O3. Ассемблерный код для get_sum на самом деле просто

movl $561, %eax
ret

Я не пробовал, но, возможно, шаблон можно расширить еще больше.

Мне кажется странным, что это работает, поскольку — по крайней мере, для моих человеческих глаз — код теперь выглядит намного сложнее. К сожалению, это довольно навязчивый способ принудительного развертывания. Флаг компилятора был бы намного лучше.

person 5gon12eder    schedule 17.09.2014
comment
К сожалению, этот обходной путь не работает для моего реального кода, но интересно, что здесь он работает для упрощенного кода. Что действительно интересно, так это то, что это (ну, что-то очень похожее на это — во всяком случае, выполнение тела дважды за итерацию цикла) позволяет этому простому примеру оптимизироваться вплоть до CONSTANT_COUNT==256 и, возможно, выше, но это так же высоко, как Я пытался. - person jgustafson; 18.09.2014

GCC имеет большое количество неясных параметров и аргументов программы, касающихся развертывания цикла (и оптимизация). Вы можете поиграть с -funroll-loops, -funroll-all-loops, --param name=value (например, с name равным max-unroll-times ....) и т. д.

Порядок аргументов gcc имеет большое значение. Вы, вероятно, захотите сначала поставить -O3, а после него странные варианты выше.

Однако увеличение развертывания не всегда улучшает производительность.

И последнее, но не менее важное: вы можете написать свой собственный плагин GCC, который изменит критерии развертывания.

Умное использование __builtin_prefetch может повысить производительность, см. этот ответ (но использование его неосторожно< /em> будет ухудшать производительность)

Вам нужно провести бенчмаркинг. Я считаю, что преждевременная микрооптимизация — это большая потеря вашего времени.

person Basile Starynkevitch    schedule 18.09.2014