gcc и оптимизация
Jan. 22nd, 2006 03:25 pmОдно из наиболее странных свойств gcc - оптимизация по принципу "или всё, или ничего". Чтобы пользоваться отладчиком, оптимизацию надо выключить. Но при этом он начинает генерировать такой код, что хоть святых выноси - восемь mov на одно значение вместо одного, сохранение значения из регистра в стек с тем чтобы тут же прочитать его из стека в регистр, и тому подобные ужасы. Чтобы получить более-менее нормальный код, надо дать хотя бы -O, но тогда начнётся смещение выполняемых действий относительно меток строк и пошаговая отладка станет невозможной. Ладно, для свежеиспечённого изделия такое понятно, но ведь gcc скоро стукнет 20 лет. Неужели за это время никто не озаботился проблемой качества кода на нижних уровнях оптимизации? Любой коммерческий компилятор (даже Borland'овский) справляется с этим значительно лучше gcc.
А самый злобный пример на его недооптимизацию выглядит так. Возьмём простейшую main():
Сассемблируем (результат одинаков для 3.4, 4.0, 4.1):
Что это за кошмар с вычислениями, которые можно было сделать на этапе компиляции?? Ах, это -mpreferred-stack-boundary=4... При подъёме до -O код становится вменяемым:
Только не надо говорить, что отладчик - сакс. Луговского слышали, всё понятно. Отладчик - рулез, если использовать его по назначению. Например, по срабатыванию breakpoint'а двигаться дальше сделав отладочную печать, или проверять условие и при его невыполнении возобновлять выполнение. (Второе не является watchpoint, по крайней мере в смысле gdb.) Есть ещё много вариантов, которые не сводятся к тупому втыканию в экран. Хотя и оно иногда полезно - чтобы наткнуться на проблему которую не замечал в упор из-за "замыленного глаза".
Насколько сложно было бы сделать в gcc нормальную промежуточную оптимизацию, действующую в пределах одной строки? И стоит ли оно возни? Смотря на коммерческие компиляторы я думаю, что стоит. И в чём загвоздка?
А самый злобный пример на его недооптимизацию выглядит так. Возьмём простейшую main():
int main()
{
int n, a, b;
return 0;
}
Сассемблируем (результат одинаков для 3.4, 4.0, 4.1):
main:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
movl $0, %eax
leave
ret
Что это за кошмар с вычислениями, которые можно было сделать на этапе компиляции?? Ах, это -mpreferred-stack-boundary=4... При подъёме до -O код становится вменяемым:
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
subl $16, %esp
movl $0, %eax
leave
ret
Только не надо говорить, что отладчик - сакс. Луговского слышали, всё понятно. Отладчик - рулез, если использовать его по назначению. Например, по срабатыванию breakpoint'а двигаться дальше сделав отладочную печать, или проверять условие и при его невыполнении возобновлять выполнение. (Второе не является watchpoint, по крайней мере в смысле gdb.) Есть ещё много вариантов, которые не сводятся к тупому втыканию в экран. Хотя и оно иногда полезно - чтобы наткнуться на проблему которую не замечал в упор из-за "замыленного глаза".
Насколько сложно было бы сделать в gcc нормальную промежуточную оптимизацию, действующую в пределах одной строки? И стоит ли оно возни? Смотря на коммерческие компиляторы я думаю, что стоит. И в чём загвоздка?
no subject
Date: 2006-01-22 02:38 pm (UTC)В данном случае, как раз проблема в том, что компилятор вперед не заглядывает, и прошлого тоже не помнит.
no subject
Date: 2006-01-22 02:55 pm (UTC)В пределах одной строки исходного файла, то есть между метками "тут начинается код строки N".
Когда идёт последовательность команд вида
movl %eax,%ebx
movl %ebx,%eax
movl %eax,%ebx
(напоминаю, что в AT&T приёмник справа, в отличие от Intel syntax)
или даже таких:
movl %eax,-16(%esp)
movl -16(%esp),%eax
movl -16(%esp),%ebx
это во-первых заметные тормоза (особенно в случае памяти... кэш конечно помогает, но это всё равно медленнее прямого копирования), во-вторых, становится просто грустно.
> В данном случае, как раз проблема в том, что компилятор вперед не заглядывает, и прошлого тоже не помнит.
В общем-то он всё помнит. Если его попросить эту память применять. Чем выше уровень оптимизации, тем более серьёзная обработка применяется.
no subject
Date: 2006-01-22 06:58 pm (UTC)Просто, насколько я понимаю, в GCC оптимизация происходит очень поздно, когда от строк осталось уже одно воспоминание.
no subject
Date: 2006-01-23 09:19 am (UTC)То есть задача - не дать оптимизациям перейти границы этих меток.
no subject
Date: 2006-01-22 02:43 pm (UTC)no subject
Date: 2006-01-22 03:11 pm (UTC)Из явных отсталостей этой версии - непонимание новых свойств i686 (хотя target PPro у него есть): того что в i686 команды push/pop крайне дороги из-за своих зависимостей по [e]sp, и их надо максимально заменять на прямые записи по смещению от ebp или esp; технологии переименования регистров. Но это можно списать на возраст (датирована 98-м годом, а реально можно считать, что кодогенератор там максимум 97-го года - тогда P3 ещё не было, P2 только начал распространяться в народ).
no subject
Date: 2006-01-22 06:23 pm (UTC)no subject
Date: 2006-01-29 10:45 am (UTC)no subject
Date: 2006-01-29 08:24 pm (UTC)no subject
Date: 2006-01-29 08:49 pm (UTC)http://groups.google.com/group/alt.russian.z1/msg/8f99a5cd492f5a3d
no subject
Date: 2006-01-29 10:24 pm (UTC)no subject
Date: 2006-01-23 09:12 am (UTC)6.0 - это старо как мир :)
no subject
Date: 2006-01-29 08:24 pm (UTC)Брал утверждение про дороговизну отсюда (а ещё лучше тут, выглядело оно настолько естественно, что не усомнился. Должен был усомниться? А как тогда оно ухитряется (зависимость действительно нетривиальна)?
no subject
Date: 2006-01-29 09:32 pm (UTC)Сейчас лень лезть в optimization guide. Насколько я помню, в П4, архитектуру которого я знаю лучше чем i686, PUSH раскладывается в два микроопа - ALU: двинуть ESP и MEMSTORE: засунуть значение в память. В случае с MOV-ом, имеем только второй.
Фишка в том, что у P4 есть два ALU, каждый из которых может выполнять две ALU инструкции за такт, параллельно с юнитом MEMSTORE, который может выполнить одно засовывание за такт. В результате несколько последовательных пушей загружают исполняющие юниты след. образом:
Имеем по одному такту на PUSH - столько же, сколько и MOV, который загружает только MEMSTORE. Благодаря оптимизации архитектуры, кривизна PUSH не наносит урона :) Ну разве что гипертрединг тормозит, да процессор греется сильнее.
no subject
Date: 2006-01-29 09:45 pm (UTC)void qq( const char* f, const char* a1, const char* a2 )
{
printf( f, a1, a2 );
mmap(0, 1, 2, 3, 4, 5);
}
Проверяем через icc 9.0.
Тест 1:
$ icc -S s5.c -O0 -march=pentium4
В выводе:
..B1.1: # Preds ..B1.0
pushl %ebp #2.1
movl %esp, %ebp #2.1
addl $-12, %esp #3.3
movl 8(%ebp), %eax #3.11
movl %eax, (%esp) #3.11
movl 12(%ebp), %eax #3.14
movl %eax, 4(%esp) #3.14
movl 16(%ebp), %eax #3.18
movl %eax, 8(%esp) #3.18
call printf #3.3
Тест 2:
$ icc -S s5.c -O3 -march=pentium4
В выводе:
..B1.1: # Preds ..B1.0
pushl 12(%esp) #2.1
pushl 12(%esp) #2.1
pushl 12(%esp) #2.1
call printf #3.3
Моя совсем перестала понимать политику партии.
no subject
Date: 2006-01-29 09:56 pm (UTC)no subject
Date: 2006-01-29 10:03 pm (UTC)no subject
Date: 2006-01-29 10:10 pm (UTC)no subject
Date: 2006-01-29 09:41 pm (UTC)no subject
Date: 2006-01-22 02:56 pm (UTC)no subject
Date: 2006-01-22 03:18 pm (UTC)При этом всём gcc заметно хуже генерирует (по многочисленным отзывам; я сам не проверял) по сравнению с Intel C, а порой и с MS C.
no subject
Date: 2006-01-22 03:51 pm (UTC)Консольный gdb при -O2 у меня много раз показывает одну и ту же строчку, пока, наконец, не проинициализирует все переменные. Но при этом честно пишет номера строк, и видно, что они немонотонны.
no subject
Date: 2006-01-22 04:00 pm (UTC)main: pushl %ebp movl %esp,%ebp subl $24,%esp xorl %eax,%eax jmp .L2 .p2align 4,,7 .L2: leave ret .size main , . - main% gcc -v
Reading specs from /usr/lib/gcc-lib/i386-unknown-openbsd3.5/2.95.3/specs
gcc version 2.95.3 20010125 (prerelease, propolice)
no subject
Date: 2006-01-22 04:19 pm (UTC)В gcc3 это допущение убрали и сделали вычисление при создании каждого фрейма.
no subject
Date: 2006-01-22 06:34 pm (UTC)-O turns on the following optimization flags: -fdefer-pop -fmerge-constants -fthread-jumps -floop-optimize -fcrossjumping -fif-conversion -fif-conversion2 -fdelayed-branch -fguess-branch-probability -fcprop-registers -O also turns on -fomit-frame-pointer on machines where doing so does not interfere with debugging.Видимо, тебе нужно подмножество этого набора. Может, без послених пяти или типа того.
no subject
Date: 2006-01-22 07:47 pm (UTC)Проверено для 3.3.4 и на следующем коде:
int f(int a, int b, int c, int d)
{
int x, y, z;
x = a + 1;
y = a + 1;
z = a + 1;
return x * y - z;
}
С -O оно все вычисления прорабатывает и в машинный код переводит уже простое tmp1:=a+1, eax:=tmp1*tmp1+tmp1. С этими флагами по отдельности - не делает этого. Так что тут какие-то более злобные хитрости. Покопаюсь, интересно же блин.
Впрочем, забегая наперёд: даже если бы это сработало - было бы методически некорректным решением задачи. Состав-то флагов может меняться с каждой версией...
no subject
Date: 2006-01-23 08:09 am (UTC)Посмотреть по доке, какой набор опций включает -O на 3.3.4, и попробовать их вырубать по одной.
Если не поможет, пробовать методом дихотомии вырубать множество оставшихся.
no subject
Date: 2006-01-23 09:17 am (UTC)Если бы я записал все эти -fxxx в командную строку и оно повторило бы результат -O, было бы откуда плясать подобной дихотомией, последовательным отключением или ещё как. Но оно не повторяет! При -O оптимизация включается, а при опциях по отдельности - нет:(
no subject
Date: 2006-01-23 09:22 am (UTC)no subject
Date: 2006-01-23 09:42 am (UTC)Сгенерированный код не изменился - аналога -O не наблюдается.
Вывод: info недосказывает, когда говорит, что -O "turns on" группу флагов: включаются не только они, и читать что -O аналогично этой группе флагов - нельзя.
Просмотр кода gcc (grep -w optimize *.c) показывает достаточно много мест, где переменная optimize проверяется напрямую, а не через отдельные субопции.
no subject
Date: 2006-01-23 09:49 am (UTC)Видимо, даже самые лучшие опенсорсный продукт неизбежно накрывает ползучим фичеризмом и поганым кодом.
С третьей стороны, ты можешь сделать патч или хотя бы засабмитить баг :) Правда, он наверняка пойдёт в Duplicate.
no subject
Date: 2006-01-23 09:52 am (UTC)Или наоборот - это недочищенные остатки начального состояния.
> С третьей стороны, ты можешь сделать патч или хотя бы засабмитить баг :) Правда, он наверняка пойдёт в Duplicate.
Скорее steer committee посмотрит и скажет "ну и какая с того польза?"
Пожуём - увидим.
no subject
Date: 2006-01-23 11:28 am (UTC)Turbo Pascal
Date: 2006-01-23 09:10 am (UTC)joss
Re: Turbo Pascal
Date: 2006-01-23 09:22 am (UTC)Во-вторых: да, хочу - в ряде случаев, а именно - при отладке - но не с этими средствами. Ну негде мне применить Delphi, основной код - Си. А отладка регулярно нужна. А оптимизация - тоже - некоторые задачи без -O фатально загибаются по быстродействию.
Re: Turbo Pascal
Date: 2006-01-24 08:58 am (UTC)В Watcom при включении отладки оптимизация отваливается - это раз, в gcc это так же.
Если надо отладка и оптимизация - то разбей код на две группы : одну компиль
с оптимизацией, другую с отладкой - постепенно уменьшай 2-ю.
И ввобще - лучшая оптимизация - это лучший алгоритм :-P
Или новый процессор :)
Re: Turbo Pascal
Date: 2006-01-24 10:06 am (UTC)> Или новый процессор :)
Я тебя тоже очень люблю. ;-|
no subject
Date: 2006-01-29 10:30 pm (UTC)