2016-09-13 3 views
9

Этот вопрос частично является следующим вопросом: GCC 5.1 Loop unrolling.Реактивное поведение в GCC

Согласно GCC documentation, и, как указано в моем ответе на поставленный вопрос, флаги, такие как -funroll-loops очередь на «полного пилинга петли (т.е. полного удаления петель с малым постоянным числом итераций)». Поэтому, когда такой флаг включен, компилятор может выбрать разворачивание цикла, если он определит, что это оптимизирует выполнение данного фрагмента кода.

Тем не менее, в одном из моих проектов я заметил, что GCC иногда разворачивает циклы , хотя соответствующие флаги не были включены. Например, рассмотрим следующий простой фрагмент кода:

int main(int argc, char **argv) 
{ 
    int k = 0; 
    for(k = 0; k < 5; ++k) 
    { 
    volatile int temp = k; 
    } 
} 

При компиляции с -O1, цикл развернутого и следующий код сборки генерируется с любой современной версией GCC:

main: 
     movl $0, -4(%rsp) 
     movl $1, -4(%rsp) 
     movl $2, -4(%rsp) 
     movl $3, -4(%rsp) 
     movl $4, -4(%rsp) 
     movl $0, %eax 
     ret 

Даже когда компиляция с дополнительным -fno-unroll-loops -fno-peel-loops, чтобы убедиться, что флаги отключен, GCC неожиданно выполняет цикл разворачивания по описанному выше примеру.

Это наблюдение приводит меня к следующим тесно связанным вопросам. Почему GCC выполняет разворот цикла, даже если флаги, соответствующие этому поведению, отключены? Развертывание также контролируется другими флагами, которые могут заставить компилятор развернуть цикл в некоторых случаях, хотя -funroll-loops отключен? Есть ли способ полностью отключить разворот цикла в GCC (часть от компиляции с -O0)?

Интересно Clang компилятор имеет ожидаемое поведение здесь, и, кажется, только выполнять разворачивая когда -funroll-loops включена, а не в других случаях.

Заранее спасибо, любые дополнительные сведения по этому вопросу были бы очень признательны!

+0

Поздравляем. Вы обнаружили, что разные компиляторы отличаются поведением и что флаги, которые вы передаете им, не всегда означают то, что, по вашему мнению, они имеют в виду. Добро пожаловать в реальный мир. –

+0

Разве это нарушает функциональность вашей программы? – Serge

+0

Нет, это не нарушает функциональность. Это больше вопрос общего интереса к тому, как GCC выполняет цикл разворачивания и как настраивать это поведение. – Pyves

ответ

7

Почему GCC выполняет цикл разворачивания, даже если флаги , соответствующие этому поведению, отключены?

Подумайте об этом с прагматичного взгляда: что вам нужно, передавая такой флаг компилятору? Ни один разработчик C++ не попросит GCC развернуть или не разворачивать циклы, просто для того, чтобы иметь петли или нет в коде сборки, есть цель. Целью с -fno-unroll-loops является, например, жертвовать небольшой скоростью, чтобы уменьшить размер вашего двоичного файла, если вы разрабатываете встроенное программное обеспечение с ограниченным хранилищем. С другой стороны, цель с -funrool-loops состоит в том, чтобы сообщить компилятору, что вы не заботитесь о размере вашего двоичного кода, поэтому он не должен колебаться, чтобы разворачивать циклы.

Но это не значит, что компилятор будет слепо развернуть или не все ваши петли!

В вашем примере, причина проста: петля содержит только один инструкцию - несколько байт на любых платформах - и компилятор знает, что это negligeable и будет в любом случае займет почти такой же размер, как код сборки, необходимый для петля (sub + mov + jne на x86-64).

Вот почему GCC 6.2, с -O3 -fno-unroll-loops превращает этот код:

int mul(int k, int j) 
{ 
    for (int i = 0; i < 5; ++i) 
    volatile int k = j; 

    return k; 
} 

... следующий код сборки:

mul(int, int): 
    mov DWORD PTR [rsp-0x4],esi 
    mov eax,edi 
    mov DWORD PTR [rsp-0x4],esi 
    mov DWORD PTR [rsp-0x4],esi 
    mov DWORD PTR [rsp-0x4],esi 
    mov DWORD PTR [rsp-0x4],esi 
    ret  

Он не слушает вас, потому что это будет (почти , в зависимости от архитектуры) не изменяет размер двоичного файла, но быстрее. Тем не менее, если вы увеличиваете чуток ваш счетчик цикла ...

int mul(int k, int j) 
{ 
    for (int i = 0; i < 20; ++i) 
    volatile int k = j; 

    return k; 
} 

... это следует за подсказку:

mul(int, int): 
    mov eax,edi 
    mov edx,0x14 
    nop WORD PTR [rax+rax*1+0x0] 
    sub edx,0x1 
    mov DWORD PTR [rsp-0x4],esi 
    jne 400520 <mul(int, int)+0x10> 
    repz ret 

вы получите такое же поведение, если вы держите свой счетчик цикла в 5 но вы добавляете код в цикл.

Подводя итог, подумайте обо всех этих флагах оптимизации как hint для компилятора и с прагматичной точки зрения разработчика. Это всегда компромисс, а когда вы строите программное обеспечение, вы никогда хотите спросить у все или no цикл разворачивания.

В качестве окончательного примечания, еще одним очень похожим примером является флаг -f(no-)inline-functions. Я каждый день борюсь с компилятором за встроенные (или нет!) Некоторые из моих функций (с ключевым словом inline и __attribute__ ((noinline)) с GCC), и когда я проверяю код сборки, я вижу, что этот smartass все еще иногда делает то, что он хочет, когда я хочу встроить функцию, которая определенно слишком длинна для ее вкуса. И большую часть времени, это правильная вещь, и я счастлив!

+0

По крайней мере, компиляторы * do * обычно слушают '__attribute__ (((нет) inline))' и прочее, как быстрая/строгая математика. Я не могу представить, чтобы компилятор обошел игнорирующий строгий математический флаг. – Mysticial

Смежные вопросы