Сегодня на /r/C_Programming задали вопрос о влиянии const
в C на оптимизацию. Я много раз слышал варианты этого вопроса в течении последних двадцати лет. Лично я обвиняю во всём именование const
.
Рассмотрим такую программу:
void foo(const int *);
int
bar(void)
{
int x = 0;
int y = 0;
for (int i = 0; i < 10; i++) {
foo(&x);
y += x; // this load not optimized out
}
return y;
}
Функция foo
принимает указатель на const, который обещает от имени автора foo
что значение x
не будет изменено. Может показаться, что компилятор может предположить, что x
всегда равен нулю, а значит и y
тоже.
Однако, если мы посмотрим на ассемблерный код, генерируемый несколькими разными компиляторами, то увидим, что x
загружается при каждой итерации цикла. Вот что выдал gcc 4.9.2 с -O3, с моими комментариями:
bar:
push rbp
push rbx
xor ebp, ebp ; y = 0
mov ebx, 0xa ; цикл по переменной i
sub rsp, 0x18 ; allocate x
mov dword [rsp+0xc], 0 ; x = 0
.L0: lea rdi, [rsp+0xc] ; вычисляем &x
call foo
add ebp, dword [rsp+0xc] ; y += x (не оптимизировано?)
sub ebx, 1
jne .L0
add rsp, 0x18 ; deallocate x
mov eax, ebp ; возвращаем y
pop rbx
pop rbp
ret
clang 3.5 (с -fno-unroll-loops) выдал примерно то же самое, только ebp и ebx поменялись местами, и вычисление &x
вынесено из цикла в r14
.
Неужели оба компилятора не способны воспользоваться этой полезной информацией? Разве если foo
изменит x
, это не будет undefined behavior? Как ни странно, ответ — "нет". В этой ситуации, это будет абсолютно верным определением foo
.
void
foo(const int *readonly_x)
{
int *x = (int *)readonly_x; // cast away const
(*x)++;
}
Важно помнить, что const
— не значит константный. Возьмите себе на заметку, что это неправильное название. Это не инструмент для оптимизации. Он нужен чтобы информировать программиста — а не компилятор — как инструмент, помогающий поймать определенный класс ошибок во время компиляции. Мне нравится, когда его используют в API потому что он сообщает, как функция будет использовать свои аргументы, или как вызывающий должен обращаться с возвращенными указателями. Обычно он недостаточно строгий, чтобы изменить поведение компилятора.
Несмотря на то, что я сказал, иногда компилятор может воспользоваться const
для оптимизации. В спецификация C99, в §6.7.3¶5, есть одно предложение об этом:
Если сделана попытка изменить объект объявленный с модификатором const через использование lvalue без модификатора const, поведение неопределенно.
Исходный x
был без модификатора const, поэтому это правило не применилось. И нет никакого правила против приведения к не-const
типу, чтобы изменить объект который сам по себе не const
. Это значит, что вышеприведённое поведение foo
это не undefined behavior для этого вызова. Обратите внимание, что неопределенность foo
зависит от того, как она была вызвана.
Одним изменением в bar
я могу сделать это правило применимым, позволяя оптимизатору поработать.
const int x = 0;
Компилятор теперь может предположить, что изменение x
в foo
— это undefined behavior, и потому никогда не происходит. Как бы то ни было, в основном так оптимизатор C рассуждает о ваших программах. Компилятор может предположить, что x
никогда не изменяется, позволяя ему оптимизировать и загрузку в каждой итерации, и y
.
bar:
push rbx
mov ebx, 0xa ; переменная цикла i
sub rsp, 0x10 ; allocate x
mov dword [rsp+0xc], 0 ; x = 0
.L0: lea rdi, [rsp+0xc] ; вычисляем &x
call foo
sub ebx, 1
jne .L0
add rsp, 0x10 ; deallocate x
xor eax, eax ; возвращаем 0
pop rbx
ret
Загрузка исчезает, y
исчезает, и функция всегда возвращает ноль.
Любопытно, что спецификация позволяет компилятору пойти еще дальше. Он может разместить x
где-нибудь вне стека, даже в read-only памяти. Например, он может произвести такую трансформацию:
static int __x = 0;
int
bar(void)
{
for (int i = 0; i < 10; i++)
foo(&__x);
return 0;
}
Или на x86-64 (-fPIC, модель малой памяти), где получается избавиться от еще нескольких инструкций:
section .rodata
x: dd 0
section .text
bar:
push rbx
mov ebx, 0xa ; переменная цикла i
.L0: lea rdi, [rel x] ; вычисляем &x
call foo
sub ebx, 1
jne .L0
xor eax, eax ; возвращаем 0
pop rbx
ret
Ни clang, ни gcc не заходят так далеко, видимо потому, что это более опасно для плохо написанного кода.
Даже со специальным правилом о const
rule, используйте const
для себя и своих товарищей-программистов. Пусть оптимизатор сам для себя решает, что константно, а что нет.
Комментарии (28)
DrLivesey
07.08.2016 11:32В последнее время меня интересует вопрос о влиянии const на время компиляции. Логика заключается в следующем: если компилятору сообщить что переменная определена только для чтения, то это должно отсеять некоторые заведомо некорректные ветки в процессе оптимизации, а следовательно и сократить время работы компилятора.
maaGames
07.08.2016 14:40Заведомо некорректные ветки приведут к ошибке компиляции.
DrLivesey
07.08.2016 14:54Вероятно, я неверно выразился.
Скажем, компилятор проверяет можно ли «выбросить» переменную (в предположенни что она используется в данной области видимости, но не изменяется). Если переменную определить без «const» то придется просмотреть всю область видимости чтобы убедиться в возможности такого действия, в противном случае просматривать область видимости не придется (хотя это еще вопрос — надо же определить, что переменная не подвергается модификации).maaGames
07.08.2016 14:57Всё равно придётся просматривать всю область видимости на наличие const_cast и (). Да даже если и выкидывать, время компиляции обычно итак меньше времени оптимизации и компоновки, особенно если кодогенерация отложена на этап компоновки.
gbg
07.08.2016 20:49+3Нетушки. Если кто-то скинул const при помощи const_cast и потом попытался записать в полученную сущность, компилятор может с превеликим удовольствием влепить UB:
$5.2.11/7 — "[Note: Depending on the type of the object, a write operation through the pointer, lvalue or pointer to data member resulting from a const_cast that casts away a const-qualifier68) may produce undefined behavior (7.1.5.1). ]"
В зависимости от типа объекта, операция записи в указатель, ссылку или указатель на поле данных, полученные путем снятия квалификатора const с помощью const_cast, может привести к неопределенному поведению.maaGames
08.08.2016 14:04+1Т.к. это UB, то точно ничего не скажу, но если говорить о практическом использовании, то если объект изначально не константный, то использовать const_cast относительно безопасно. Т.е. если неконстантный объект передать в функцию по указателю на константу, то от избавление от константности ничего плохого может не сделать.
И вообще, мы тут спорим о том, как кошернее говнокодить. Не будем так.)
Daniro_San
07.08.2016 11:54+5Константы это хорошо.
Серьезно
int main(const int argc, const char *const *const argv) {}
ELEKTRO_YAR
07.08.2016 14:36Конечно незначительная мелочь, но можно исправить. В самом начале статьи код вставлен не как С, и поэтому все что после символа ";" в объявлении цикла for воспринимается как комментарий.
A-Stahl
07.08.2016 16:03-10Я уж лет 10 пишу код, но с const так толком и не подружился и использую его крайне редко лишь в тех случаях чтобы самому не забыть, что что-то менять не имеет смысла или это менять нужно где-то в другом специально отведённом месте.
А от синтаксиса const рядом с указателями так и вовсе плакать хочется.
Ну его, короче, в пень:)
vanxant
07.08.2016 19:54+2Стандарт написан абсолютно правильно, а всю статью можно свести к одному абзацу:
Неважно, что написано в объявлении функции, важно, что фактически в неё передаётся здесь и сейчас.
Был бы x изначально объявлен как const, то все оптимизации бы сработали, а за трюк с const_cast функция foo получила бы по башке (то самое UB).
Т.е. компилятор/оптимизатор смотрит не на декларации о намерениях, а на факты. Что и должно быть.lemelisk
07.08.2016 20:39На мой взгляд, суть статьи в том, что компилятор не имеет права делать предположение (и соответственно, проводить исходя из этого какие-то оптимизации), что если нечто передается внутрь функции по указателю на константу (константной ссылке), то оно внутри этой функции по этому указателю не будет изменено (естественно, в том случае, когда реализацию функции он посмотреть не может). Это довольно контринтуитивно, хотя и логично, если вспомнить о наличии функционала аля
const_cast
, которым мы должны иметь возможность воспользоваться.vanxant
08.08.2016 03:36Ладно, на пальцах. В статье слишком упростили и «выкинули вместе с водой ребёнка».
Предположим, у нас есть некая железяка — пылесос или марсоход, неважно. И мы пишем под неё под шланг или gcc
1. Есть внешняя либа от разработчиков железа, предоставляющая функцию foo(const int*)
2. Либа, на самом деле, написана на асме или чём-то таком, где про const не слышали, но сделали extern «C» void foo(...)
3. Предположим, аргумент foo — нифига не int*, а указатель на какую-нибудь хитромудрую структуру. Обычно в жизни так и бывает, но в статье упростили.
4. Так-то вообще эта структура const, но для целей отладки в неё добавили пару отладочных полей. Ну там счетчиков, не знаю чего. Типа mutable-полей в С++. Есть одна версия *.h файлов, но две версии либы — «релиз» и «дебаг». И в дебаге const_cast неизбежен. Но компилятор видит только хэдеры и решает по факту.
5. В дебаг-версии всё работает. Заливаем код в ROM для пылесоса или марсохода, отправляем в релиз… и опа былинный фейл.
armature_current
07.08.2016 20:41+4А еще такой модификатор говорит линковщику перенести данные во Flash сектора, если речь идет о встраиваемых системах. В микроконтроллерах процедура изменения flash-секторов несколько сложнее, особенно для более ранних поколений, где вообще ПЗУ прошивалась ультрафиолетом. Но то было раньше, а сейчас это часто используется для указания места хранения больших таблиц данных, например значений тригонометрических функций. Можно конечно и через линковщик выделить, но приписать const гораздо быстрее. Обычно, в МК ram-памяти значительно меньше, чем flash`ки. Так что для embedded систем const очень даже константный, а не просто в помощь программисту.
rrrav
07.08.2016 22:54Да, в нормальных микроконтроллерах достаточно указать Const, и данные лежат во флэше. А вот AVR не поймет — ему надо для этого указывать атрибут PROGMEM (с адресацией там намудрили).
LynXzp
08.08.2016 02:44Когда «PROGMEM =» а когда и "=PSTR()", и читать потом через «pgm_read_byte()». А если нужно прочитать int… но в принципе тоже ничего, а вот в MikroC нужно вручную сказать по какому адресу будет лежать массив в flash.
Antervis
07.08.2016 21:09+3в с++ в некоторых случаях (например, так реализованы контейнеры в Qt) «лишний» const позволит вызвать правильную, более быструю перегрузку того или иного метода. Поэтому эффект на производительность всё-таки имеется. Ну а в большинстве случаев это просто дополнительный предохранитель от стрельбы себе в ногу. WinAPI, кстати, много где грешит тем, что принимает неконстантные указатели на неизменяемые строки.
vanxant
08.08.2016 04:16-5К сожалению, нормальная перегрузка const/не-const функций в плюсах так и не описана. Слишком сложно получается.Ну т.е. где-то оно работает, но в слишком особенных случаях на особенных компиляторах. Вот так взять и написать два метода, один из которых будет конст, а второй — нет, и чотбы оно автоматически выбиралось — нельзя.
Antervis
08.08.2016 05:44в одну сторону то точно везде работает: не может же для const объекта вызваться мутирующий метод…
mkarev
По-моему тут все хорошо
Судя по объявлению foo — глобальная нестатическая ф-ция, т.е. мы не знаем что там происходит и не знаем «смоет» она регистры или нет (те, что можно не сохранять).
А «x» — локальная переменная, выделенная на стеке.
«x» передается в foo одним из аргументов (через стек или регистр).
Но при этом foo не обязана по возвращению восстановить это входное значение «x».
Поэтому, компилятор перестраховывается и делает повторную загрузку.
Temtaime
Для того, чтобы узнать, «смоет или нет» — есть calling convention.
lemelisk
Идея в том, что внутри bar x не меняется, а в foo передается через указатель на константу, поэтому «вроде как» внутри этой функции тоже не может менятся. Из этого компилятор должен бы сделать вывод, что x всегда 0 и нет никакого смысла прибавлять этот 0 к y.
mkarev
Да, признаю, про указатель не доглядел.