C компиляторы и оптимизация петли

У меня нет большого опыта с тем, как компиляторы на самом деле оптимизируют, и что различие между разными уровнями (-O2 против-O3 для gcc, например). По сути, я не уверен, эквивалентны ли следующие два заявления для произвольного компилятора:

for(i=0;i<10;++i){
variable1*variable2*gridpoint[i];
}

и

variable3=variable1*variable2;
for(i=0;i<10;++i){
variable3*gridpoint[i];
}

From a processing-time point of view it would make sense to only compute the product of variable1 и variable2 once as they don't change in the loop. This requires extra memory however, и I'm not sure how strongly an optimiser factors this overhead in. The first expression is the easiest to read if you have an equation from a paper/book и want to translate it to something computer-readable, but the second might be the fastest - especially for more complicated equations with a lot of unchanged variables within the loop (I have some pretty nasty non-linear differential equations which I would like to be human readable in the code). Does any of this change if I declare my variables as constants? I hope my question makes sense for an arbitrary compiler since I use both gcc, Intel и Portlи compilers.

7
nl ja de
@Dancrumb спасибо за ваш выше определения изменчивых. Это помогло мне постараться не идти для другого поиска значения изменчивого ключевого слова.:)
добавлено автор Sibi Rajasekaran, источник
Для отчета , изменчивый , переменная - переменная, которая связана с областью пространства памяти, которое может измениться за пределами нормального потока кода..., например, переменная, которая наносит на карту к входному регистру для микропроцессора или переменной, которая наносит на карту к регистру часов. Таким образом, каждый раз, когда вы получаете доступ к нему, это, возможно, изменилось, даже при том, что никакой код, возможно, не назначил стоимость на него. Таким образом, компилятор won' t выносят его за скобки к за пределами петли.
добавлено автор Dancrumb, источник
В этом случае, если переменная не отмечена изменчивая, целая петля будет устранена, поскольку нет никаких побочных эффектов в выражении в теле петли.
добавлено автор Jonathan Leffler, источник
@user - Только, чтобы показать, что делают компиляторы, посмотрите на этот другой ответ где 10 линий inlined вызовов функции и шаблонов превращаются всего в 4 или 5 машинных команд. Компилятор может оптимизировать петли в своем сне!
добавлено автор Bo Persson, источник
Это называют "общим устранением подвыражения".
добавлено автор Cat Plus Plus, источник
Или устранение невыполняемого кода как два из вышеупомянутых отрывков может быть безопасно устранено из программы, поскольку они ничего не делают:)
добавлено автор user405725, источник
Ответ - то, что компилятор "может" сделать любую эту оптимизацию, но не требуется, чтобы и может не сделать ни одного из них по различным причинам. Вместо того, чтобы предположить, что что-то будет оптимизировано, напишите лучший код, что вы можете.
добавлено автор Variable Length Coder, источник

3 ответы

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

С точки зрения времени обработки имело бы смысл только вычислять продукт variable1 и variable2 однажды, поскольку они не изменяются в петле.

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

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

Учитывая, что нет никакого побочного эффекта, и компилятор в состоянии определить его (в вашем примере, нет ничего стоящего способом), два из отрывков эквивалентны в этом отношении (я сверился с лязгом:-)).

Это требует дополнительной памяти, однако, и я не уверен, как сильно оптимизатор включает это наверху в.

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

Первое выражение является самым легким прочитать..

И GCC и Лязг выполнят эту оптимизацию. Я не уверен в других компиляторах, тем не менее, таким образом, необходимо будет проверить на себя. Но трудно вообразить любой хороший компилятор, который не делает устранения подвыражения.

Какое-либо из этого изменяется, если я объявляю свои переменные как константы?

Это может. Это называют константным выражением — выражение, которое содержит только константы. Константное выражение может быть оценено во время компиляции, а не во время выполнения. Так, например, если вы, которых предварительно вычислят многократный A, B и C, где и A и B - константы, компилятор <кодируете> A*B выражение только многократный C против той предварительно вычисленной стоимости. Компиляторы могут также сделать это даже с непостоянными величинами, если они могут определить его стоимость во время компиляции и быть уверены, что это не изменяется. Например:

$ cat test.c
inline int foo(int a, int b)
{
    return a * b;
}

int main() {
    int a;
    int b;
    a = 1;
    b = 2;
    return foo(a, b);
}
$ clang -Wall -pedantic -O4 -o test ./test.c
$ otool -tv ./test
./test:
(__TEXT,__text) section
_main:
0000000100000f70    movl    $0x00000002,%eax
0000000100000f75    ret

Есть также другая оптимизация, которая может произойти в случае вышеупомянутых отрывков. Ниже некоторые из них, которые приходят на ум:

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

Вторая оптимизация, которая может буквально ускорить вещи как 50 раз, использует SIMD инструкция (SSE, AVX и т.д.). Например, GCC очень хорош в нем (Intel должен быть также, если не лучше). Я проверил что следующая функция:

uint8_t dumb_checksum(const uint8_t *p, size_t size)
{
    uint8_t s = 0;
    size_t i;
    for (i = 0; i < size; ++i)
        s = (uint8_t)(s + p[i]);
    return s;
}

... is transformed into a loop where each step sums 16 values at once (i.e. as in _mm_add_epi8) with an additional code handling alignment and odd (<16) iterations count. Clang, however, have totally failed at this last time I checked. So GCC may reduce your loop that way, too, even if number of iterations are not known.

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

Я надеюсь, что это отвечает на ваши вопросы.Good Luck!

4
добавлено
"Это называют константным выражением", я думаю, что OP скорее означал, объявил ли (s) он variable1 и т.д. константа - квалифицированный.
добавлено автор Daniel Fischer, источник
@user787267: Да, это могло также помочь оптимизировать. Причина этого иногда - компилятор, не может решить, что нет никаких побочных эффектов. Например, если вы используете obj-> стоимость и делают что-то еще (т.е. вызовите некоторую функцию и проход obj в него), компилятор не будет знать, изменяется ли стоимость или не и будет всегда читать его по памяти, тогда как, если вы явно <кодируете> вар типа константы = obj-> вар; , это может держать эту стоимость в регистре вместо того, чтобы читать память и сделать другую оптимизацию. Если, однако, нет никаких побочных эффектов, и компилятор может определить его, то это doesn' t вопрос.
добавлено автор user405725, источник
Спасибо за этот очень поучительный ответ. Я действительно, однако, хотел объявлять переменные как "плавание константы variable1" и т.д., таким образом, не (обязательно) константы времени компиляции. Я могу безопасно предположить, что более чистое примечание будет также оптимизировано? Например, определение "константы пускают в ход a=NastyObject-> TediousVariableName" и использование переменной вместо утомительного имени переменной? Я can' t видят почему компилятор wouldn' t признают это простым псевдонимом
добавлено автор user787267, источник

Да, можно рассчитывать на компиляторы, чтобы сделать хорошую работу при выполнении sub устранение выражения, даже через петли. Это может привести к небольшому увеличению использования памяти, однако все это рассмотрит любой достойный компилятор, и в значительной степени всегда имеет место, что это - победа, чтобы выполнить sub устранение выражения (так как память, о которой мы говорим, является регистрами и тайником L1).

Вот некоторые быстрые тесты, чтобы "доказать" его себе также. Результаты указывают, что вы не должны в основном пытаться перехитрить компилятор, делающий руководство sub устранение выражения, просто закодировать естественно и позволить компилятору сделать то, к чему это способно (который является материалом как выяснение, какие выражения должны действительно быть устранены и которое не было должно данная целевая архитектура и окружающий код.)

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

(FTR, который использование randoms в следующем коде просто гарантирует компилятору, не может стать слишком рьяным о переменном устранении и развертывании цикла),

prog1:

#include 
#include 

int main() {
    srandom(time(NULL));
    int i, ret = 0, a = random(), b = random(), values[10];
    int loop_end = random() % 5 + 1000000000;
    for (i=0; i < 10; ++i) { values[i] = random(); }

    for (i = 0; i < loop_end; ++i) {
        ret += a * b * values[i % 10];
    }

    return ret;
}

prog2:

#include 
#include 

int main() {
    srandom(time(NULL));
    int i, ret = 0, a = random(), b = random(), values[10];
    int loop_end = random() % 5 + 1000000000;
    for (i=0; i < 10; ++i) { values[i] = random(); }

    int c = a * b;
    for (i = 0; i < loop_end; ++i) {
        ret += c * values[i % 10];
    }

    return ret;
}

И вот результаты:

> gcc -O2 prog1.c -o prog1; time ./prog1  
./prog1  1.62s user 0.00s system 99% cpu 1.630 total

> gcc -O2 prog2.c -o prog2; time ./prog2
./prog2  1.63s user 0.00s system 99% cpu 1.636 total

(Это измеряет стенное время, поэтому не обращайте внимание на 0.01 вторых различия, управляя им несколько раз они оба диапазон в 1.62-1.63 вторых диапазонах, таким образом, они - та же самая скорость),

Интересно достаточно prog1 был быстрее, когда собрано без оптимизации:

> gcc -O0 prog1.c -o prog1; time ./prog1  
./prog1  2.83s user 0.00s system 99% cpu 2.846 total

> gcc -O0 prog2.c -o prog2; time ./prog2 
./prog2  2.93s user 0.00s system 99% cpu 2.946 total

Также интересный, собирая с -o1 обеспечил лучшую работу..

gcc -O1 prog1.c -o prog1; time ./prog1 
./prog1  1.57s user 0.00s system 99% cpu 1.579 total

gcc -O1 prog2.c -o prog2; time ./prog2
./prog2  1.56s user 0.00s system 99% cpu 1.563 total

GCC и Intel - большие компиляторы и довольно умны об обработке материала как это. У меня нет опыта с Портлендским компилятором, но это довольно основные вещи для компилятора, чтобы сделать, таким образом, я был бы очень удивлен, не обращалось ли это с такими ситуациями хорошо.

3
добавлено
Не всегда хорошо использовать оптимизацию O2. Что является случаями, когда оптимизация O0 может быть полезной..? Спасибо.
добавлено автор Sibi Rajasekaran, источник
Каковы значения этих выключателей O2 O1 O0?
добавлено автор Sibi Rajasekaran, источник
Те - флаги оптимизации для GCC.-O0 не выполняет основной оптимизации,-O1 делает некоторую оптимизацию,-O2 делает еще больше оптимизации и т.д.
добавлено автор hexist, источник
O0 собирает самое быстрое и является самым легким отладить. O2 может закончить тем, что оптимизировал определения переменной и встроенный код, поэтому, когда рассматривается с отладчиком иногда вы привычка быть в состоянии осмотреть ценности некоторых переменных и такой вещи. O0 doesn' t делают любое из этого.
добавлено автор hexist, источник
We' d должны вырыть через собрание продукции, чтобы действительно выяснить what' s продолжающий O1 против O2 и нашего случая O0. Это известно, хотя это иногда оптимизация может иметь неблагоприятный эффект, но большую часть времени они приносят больше пользы, чем вреда (мы надеемся!). Если я должен был предположить, что это - вероятно, что-то тонкое как инструкция, заказывая вещь, которую CPU смог использовать в своих интересах, it' s очень незначительные различия в скорости. Don' t берут, это как показывает опыт, между прочим, в производственном коде O2 или O3 все еще, вероятно, предоставит лучший код, но можно хотеть проверить:).
добавлено автор hexist, источник
Спасибо за ваш ответ. Какие-либо предположения относительно того, почему-O1 является самым быстрым в этом случае? И почему prog1 является самым быстрым без оптимизации?
добавлено автор user787267, источник

Если бы я был компилятором, я признал бы, что обе тех петли имеют не левые операнды, и не побочные эффекты вообще, (кроме урегулирования я к 10 ), таким образом, я просто оптимизировал бы петли полностью.

Я не говорю, что это на самом деле происходит; просто похоже, что это могло произойти из кода, который вы предоставили.

0
добавлено
Это, конечно, происходит:), Но я думаю, что он просто пытался обеспечить простой пример...
добавлено автор hexist, источник
Я действительно просто пытался сделать простой пример. Я предполагаю, что это имело бы немного больше смысла, если у меня на самом деле была левая сторона в петлях.
добавлено автор user787267, источник