Цикл является Спускоподъемным все еще допустимая ручная оптимизация для кода C?

Используя последний gcc компилятор, я должен все еще думать об этих типах ручной оптимизации цикла, или компилятор будет заботиться о них для меня достаточно хорошо?

18
задан blueberryfields 30 December 2009 в 19:59
поделиться

8 ответов

Если ваш профилировщик говорит вам, что есть проблема с циклом, и только тогда нужно обратить внимание на ссылку на память в цикле, которая, как вы знаете вы, инвариантна во всем цикле, а компилятор - нет. Вот нарисованный пример, пузырящий элемент в конце массива:

for ( ; i < a->length - 1; i++)
    swap_elements(a, i, i+1);

Вы можете знать, что обращение к swap_elements не изменяет значения a->length, но если определение swap_elements находится в другом исходном файле, то вполне вероятно, что компилятор этого не делает. Следовательно, стоит поднять вычисления a->length из цикла:

int n = a->length;
for ( ; i < n - 1; i++)
    swap_elements(a, i, i+1);

На критических для производительности внутренних циклах мои ученики получают измеримые ускорения с преобразованиями типа этого.

Заметим, что поднять вычисления n-1 нет необходимости, любой оптимизирующий компилятор прекрасно способен обнаружить зацикленные вычисления среди локальных переменных. Это может быть сложнее - обращение к памяти и вызов функций. И код с n-1 более явно корректен.

Как отмечали другие, ничего подобного делать не надо, пока не профилируете и не обнаружите, что цикл - это узкое место в производительности, которое на самом деле имеет значение.

.
47
ответ дан 30 November 2019 в 05:42
поделиться

Компиляторы, как правило, отлично справляются с подобной оптимизацией, но в некоторых случаях пропускают. В общем, мой совет: пишите код как можно более читабельным (что может означать, что вы поднимаете инварианты цикла -- я предпочитаю читать написанный таким образом код), и если компилятор пропускает оптимизации, файловые ошибки, которые помогут исправить компилятор. Вставляйте оптимизацию в исходный код только в том случае, если у вас есть жесткое требование к производительности, которое не может ждать исправления компилятора, или писатели компилятора говорят, что не смогут решить проблему.

.
4
ответ дан 30 November 2019 в 05:42
поделиться

Помните правило 80-20 (80% времени выполнения в программе тратится на 20% критического кода). Оптимизация кода, не оказывающая существенного влияния на общую эффективность программы, не имеет смысла.

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

0
ответ дан 30 November 2019 в 05:42
поделиться

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

В большинстве случаев, lop hoisting - это полностью автоматический процесс, не требующий высокоуровневых знаний кода - просто много анализа жизни и зависимостей, в чем компилятор преуспевает в первую очередь.

Можно писать код, в котором компилятор не может определить, безопасно ли что-то поднимать - и в этих случаях, возможно, вы захотите сделать это сами, так как это очень эффективная оптимизация.

В качестве примера возьмем фрагмент, размещенный Стивом Джессоном:

for (int i = 0; i < 10; ++i) {
    std::cout << "The number of primes less than 1 billion is: ";
    std::cout << countPrimesLessThan(1000*1000*1000);
    std::cout << std::endl;
}

Безопасно ли поднимать вызов на countPrimesLessThan? Это зависит от того, как и где определяется функция. Что если у нее есть побочные эффекты? Это может иметь важное значение, будет ли она вызываться один или десять раз, а также при вызове . Если мы не знаем, как определяется функция, мы не можем вывести ее за пределы цикла. И то же самое, если компилятор должен выполнить оптимизацию.

Видимо ли определение функции компилятору? И достаточно ли короткое определение функции, чтобы можно было доверять компилятору ее встраивать или, по крайней мере, анализировать функцию на наличие побочных эффектов? Если да, то да, он поднимет ее за пределами цикла.

Если определение не видно, или если функция очень большая и сложная, то компилятор, скорее всего, предположит, что вызов функции не может быть безопасно перемещен, и тогда он не поднимет ее автоматически.

1
ответ дан 30 November 2019 в 05:42
поделиться

Напишите код, профилируйте его и думайте об оптимизации только тогда, когда нашли что-то недостаточно быстрое , и вы не можете придумать альтернативный алгоритм, который в первую очередь уменьшит/избавит от узкого места.

В современных компиляторах этот совет еще более важен - если вы пишете простой чистый код, то оптимизатор компилятора часто может лучше оптимизировать код, чем если попытаться дать ему "преоптимизированный" код.

.
18
ответ дан 30 November 2019 в 05:42
поделиться

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

Но, как сказали другие , вы всегда должны сначала просмотреть профиль, чтобы найти свои узкие места. Как только вы определили, что на самом деле это узкое место, только тогда вы должны проверить, выполняет ли компилятор подъем в цикле (так называемое движение по циклу с переменным кодом) в горячих точках. Если нет, то помогите.

5
ответ дан 30 November 2019 в 05:42
поделиться

Ранние оптимизации плохи только в том случае, если отрицательно сказываются на других аспектах - таких как читабельность, ясность намерений или структура.

Если вам все равно приходится объявлять это, поднятие петли может даже улучшить ясность, и это объясняет ваше предположение "это значение не меняется".

Как правило, я бы не поднимал итератор счета/завершения для std::vector, потому что это обычный сценарий, который легко оптимизировать. Я бы не поднимал ничего из того, что я могу доверять моему оптимизатору, и я бы не поднимал ничего, что, как известно, не является критичным - например, при прогоне списка из дюжины окон, чтобы ответить на нажатие кнопки. Даже если это займет 50 мс, он все равно будет выглядеть "мгновенно" для пользователя. (Но даже это опасное предположение: если новая функция требует перекручивания 20 раз по тому же коду, она вдруг становится медленной). Вы все равно должны поднимать такие операции, как открытие файлового дескриптора для добавления и т.д.

Во многих случаях - очень хорошо в петлевых подъемных операциях - очень полезно учитывать относительную стоимость: какова стоимость подъемного расчета по сравнению со стоимостью пробега через тело?


Что касается оптимизаций в целом, то достаточно много случаев, когда профилировщик не помогает. Код может иметь очень разное поведение в зависимости от пути вызова. Библиотечные писатели часто не знают свою Otr-частоту пути вызова. Изоляция куска кода для сравнения уже может существенно изменить поведение. Профилировщик может сказать "Loop X медленный", но не скажет "Loop X медленный, потому что вызов Y разбивает кэш на всех остальных". Профилировщик не смог сказать вам "этот код быстр из-за вашего хитрого процессора, но он будет медленным на компьютере Стива".


3
ответ дан 30 November 2019 в 05:42
поделиться

Там, где они могут быть важны для исполнения, вы все равно должны подумать о них.

Петлевой подъем наиболее выгоден, когда для расчета стоимости подъемника требуется много работы. Если для расчета требуется много работы, то это, скорее всего, вызов не по назначению. Если это вызов из очереди, последняя версия gcc гораздо менее вероятно, чем вы, чтобы выяснить, что она будет возвращать одно и то же значение каждый раз.

Иногда люди говорят тебе сначала составить профиль. На самом деле они не имеют в виду это, они просто думают, что если вы достаточно умны, чтобы понять, когда стоит беспокоиться о производительности, то вы достаточно умны, чтобы игнорировать их эмпирическое правило. Очевидно, что следующий код с тем же успехом может быть "преждевременно оптимизирован", независимо от того, профилирован вы или нет:

#include <iostream>

bool isPrime(int p) {
    for (int i = 2; i*i <= p; ++i) {
        if ((p % i) == 0) return false;
    }
    return true;
}

int countPrimesLessThan(int max) {
    int count = 0;
    for (int i = 2; i < max; ++i) {
        if (isPrime(i)) ++count;
    }
    return count;
}

int main() {
    for (int i = 0; i < 10; ++i) {
        std::cout << "The number of primes less than 1 million is: ";
        std::cout << countPrimesLessThan(1000*1000);
        std::cout << std::endl;
    }
}

Нужен "особый" подход к разработке программного обеспечения, чтобы не поднимать вручную вызов countPrimesLessThan из цикла, независимо от того, профилирован вы или нет.

3
ответ дан 30 November 2019 в 05:42
поделиться