
Несколько месяцев назад я упомянул в одном посте, что это миф, будто бы const помогает включать оптимизации компилятора в C и C++. Я решил, что нужно объяснить это утверждение, особенно потому, что раньше я сам верил в этот миф. Начну с теории и искусственных примеров, а затем перейду к экспериментам и бенчмаркам на реальной кодовой базе — SQLite.
Простой тест
Начнём с, как мне казалось, самого простого и очевидного примера ускорения кода на С при помощи
const
. Допустим, у нас есть два объявления функций:void func(int *x);
void constFunc(const int *x);
И, предположим, есть две версии кода:
void byArg(int *x)
{
printf("%d\n", *x);
func(x);
printf("%d\n", *x);
}
void constByArg(const int *x)
{
printf("%d\n", *x);
constFunc(x);
printf("%d\n", *x);
}
Чтобы выполнить
printf()
, процессор должен через указатель извлечь из памяти значение *x
. Очевидно, что выполнение constByArg()
может слегка ускориться, поскольку компилятору известно, что *x
является константой, поэтому нет нужды загружать её значение снова, после того как это сделала constFunc()
. Правильно? Давайте посмотрим ассемблерный код, сгенерированный GCC со включёнными оптимизациями:$ gcc -S -Wall -O3 test.c
$ view test.s
А вот полный результат на ассемблере для
byArg()
:byArg:
.LFB23:
.cfi_startproc
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movl (%rdi), %edx
movq %rdi, %rbx
leaq .LC0(%rip), %rsi
movl $1, %edi
xorl %eax, %eax
call __printf_chk@PLT
movq %rbx, %rdi
call func@PLT # The only instruction that's different in constFoo
movl (%rbx), %edx
leaq .LC0(%rip), %rsi
xorl %eax, %eax
movl $1, %edi
popq %rbx
.cfi_def_cfa_offset 8
jmp __printf_chk@PLT
.cfi_endproc
Единственное различие между ассемблерным кодом, сгенерированным для
byArg()
и constByArg()
, заключается в том, что у constByArg()
есть call constFunc@PLT
, как в исходном коде. Сам const
не привносит никаких различий.Ладно, это был GCC. Возможно, нам нужен компилятор поумнее. Скажем, Clang.
$ clang -S -Wall -O3 -emit-llvm test.c
$ view test.ll
Вот промежуточный код. Он компактнее ассемблера, и я отброшу обе функции, чтобы вам было понятнее, что я имею в виду под «никакой разницы, за исключением вызова»:
; Function Attrs: nounwind uwtable
define dso_local void @byArg(i32*) local_unnamed_addr #0 {
%2 = load i32, i32* %0, align 4, !tbaa !2
%3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %2)
tail call void @func(i32* %0) #4
%4 = load i32, i32* %0, align 4, !tbaa !2
%5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4)
ret void
}
; Function Attrs: nounwind uwtable
define dso_local void @constByArg(i32*) local_unnamed_addr #0 {
%2 = load i32, i32* %0, align 4, !tbaa !2
%3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %2)
tail call void @constFunc(i32* %0) #4
%4 = load i32, i32* %0, align 4, !tbaa !2
%5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4)
ret void
}
Вариант, который (типа) работает
А вот код, в котором наличие
const
действительно имеет значение:void localVar()
{
int x = 42;
printf("%d\n", x);
constFunc(&x);
printf("%d\n", x);
}
void constLocalVar()
{
const int x = 42; // const on the local variable
printf("%d\n", x);
constFunc(&x);
printf("%d\n", x);
}
Ассемблерный код для
localVar()
, который содержит две инструкции, оптимизированные за пределами constLocalVar()
:localVar:
.LFB25:
.cfi_startproc
subq $24, %rsp
.cfi_def_cfa_offset 32
movl $42, %edx
movl $1, %edi
movq %fs:40, %rax
movq %rax, 8(%rsp)
xorl %eax, %eax
leaq .LC0(%rip), %rsi
movl $42, 4(%rsp)
call __printf_chk@PLT
leaq 4(%rsp), %rdi
call constFunc@PLT
movl 4(%rsp), %edx # not in constLocalVar()
xorl %eax, %eax
movl $1, %edi
leaq .LC0(%rip), %rsi # not in constLocalVar()
call __printf_chk@PLT
movq 8(%rsp), %rax
xorq %fs:40, %rax
jne .L9
addq $24, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L9:
.cfi_restore_state
call __stack_chk_fail@PLT
.cfi_endproc
Промежуточный код LLVM немножко чище.
load
перед вторым вызовом printf()
была оптимизирована за пределами constLocalVar()
:; Function Attrs: nounwind uwtable
define dso_local void @localVar() local_unnamed_addr #0 {
%1 = alloca i32, align 4
%2 = bitcast i32* %1 to i8*
call void @llvm.lifetime.start.p0i8(i64 4, i8* nonnull %2) #4
store i32 42, i32* %1, align 4, !tbaa !2
%3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 42)
call void @constFunc(i32* nonnull %1) #4
%4 = load i32, i32* %1, align 4, !tbaa !2
%5 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4)
call void @llvm.lifetime.end.p0i8(i64 4, i8* nonnull %2) #4
ret void
}
Итак,
constLocalVar()
успешно проигнорировала перезагрузку *x
, но вы могли заметить нечто странное: в телах localVar()
и constLocalVar()
один и тот же вызов constFunc()
. Если компилятор может сообразить, что constFunc()
не модифицировала *x
в constLocalVar()
, то почему он не может понять, что тот же самый вызов функции не модифицировал *x
в localVar()
?Объяснение связано с тем, почему
const
в С непрактично использовать в качестве оптимизации. В C у const
есть, по сути, два возможных смысла: - она может означать, что переменная — это доступный только для чтения псевдоним каких-то данных, которые могут быть константой, а могут и не быть.
- либо она может означать, что переменная действительно является константой. Если вы отвяжете
const
от указателя на константное значение, а потом запишете в неё, то получите неопределённое поведение. С другой стороны, проблем не будет, еслиconst
является указателем на значение, не являющееся константой.
Вот поясняющий пример реализации
constFunc()
:// x is just a read-only pointer to something that may or may not be a constant
void constFunc(const int *x)
{
// local_var is a true constant
const int local_var = 42;
// Definitely undefined behaviour by C rules
doubleIt((int*)&local_var);
// Who knows if this is UB?
doubleIt((int*)x);
}
void doubleIt(int *x)
{
*x *= 2;
}
localVar()
дала constFunc()
указатель const
на не-const
переменную. Поскольку изначально переменная не была const
, то constFunc()
может оказаться лжецом и принудительно модифицирует переменную без ицициации UB. Поэтому компилятор не может предполагать, что после возвращения constFunc()
переменная будет иметь такое же значение. Переменная в constLocalVar()
действительно является const
, так что компилятор не может предполагать, что она не будет изменена, поскольку на этот раз она будет UB для constFunc()
, чтобы компилятор отвязал const
и записал в переменную.Функции
byArg()
и constByArg()
из первого примера безнадёжны, потому что компилятор никак не может узнать, действительно ли *x
является const
.Но откуда взялась несогласованность? Если компилятор может предположить, что
constFunc()
не меняет свой аргумент, будучи вызванной из constLocalVar()
, то он может применять те же оптимизации и к вызовам constFunc()
, верно? Нет. Компилятор не может предположить, что constLocalVar()
вообще когда-либо будет вызвана. И если не будет (например, потому что это просто какой-то дополнительный результат работы генератора кода или макроса), то constFunc()
может втихую изменить данные, не инициировав UB.Возможно, вам потребуется несколько раз прочитать приведённые выше примеры и объяснение. Не переживайте, что это звучит абсурдно — так и есть. К сожалению, запись в переменные
const
является худшей разновидностью UB: чаще всего компилятор даже не знает, будет ли это UB. Поэтому, когда компилятор видит const
, он должен исходить из того, что кто-то где-то может поменять его, а значит компилятор не может использовать const
для оптимизации. На практике это справедливо, потому что немало реального кода на С содержит отказ от const
в стиле «я знаю, что делаю».Короче, бывает много ситуаций, когда компилятору не дают использовать
const
для оптимизации, включая получение данных из другой области видимости с помощью указателя, или размещение данных в куче (heap). Или того хуже, обычно в ситуациях, когда компилятор не может использовать const
, это и не обязательно. К примеру, любой уважающий себя компилятор может и без const
понять, что в этом коде x
является константой:int x = 42, y = 0;
printf("%d %d\n", x, y);
y += x;
printf("%d %d\n", x, y);
Итак,
const
почти бесполезен для оптимизации, потому что:- За несколькими исключениями, компилятор вынужден игнорировать его, поскольку какой-нибудь код может на законных основаниях отвязать
const
.
- В большинстве вышеупомянутых исключений компилятор всё-равно может понять, что переменная является константой.
C++
Если вы пишете на С++, то
const
может повлиять на генерирование кода посредством перегрузки функций. У вас могут быть const
и не-const
-перегрузки одной и той же функции, и при этом не-const
могут быть оптимизированы (программистом, а не компилятором), например, чтобы меньше копировать.void foo(int *p)
{
// Needs to do more copying of data
}
void foo(const int *p)
{
// Doesn't need defensive copies
}
int main()
{
const int x = 42;
// const-ness affects which overload gets called
foo(&x);
return 0;
}
С одной стороны, я не думаю, что на практике это часто применяется в С++-коде. С другой стороны, чтобы действительно была разница, программист должен делать предположения, которые недоступны компилятору, поскольку они не гарантированы языком.
Эксперимент с SQLite3
Хватит теории и надуманных примеров. Какое влияние оказывает
const
на настоящую кодовую базу? Я решил провести эксперимент с БД SQLite (версия 3.30.0), потому что:- В ней используется
const.
- Это нетривиальная кодовая база (свыше 200 KLOC).
- В качестве базы данных она включает в себя ряд механизмов, начиная с обработки строковых значений и заканчивая преобразованием чисел в дату.
- Её можно протестировать с помощью нагрузки, ограниченной по процессору.
Кроме того, автор и программисты, участвующие в разработке, уже потратили годы на улучшение производительности, так что можно предположить, что они не пропустили ничего очевидного.
Подготовка
Я сделал две копии исходного кода. Одну скомпилировал в обычном режиме, а вторую предварительно обработал с помощью хака, чтобы превратить
const
в холостую команду:#define const
(GNU)
sed
может добавить это поверх каждого файла с помощью команды sed -i '1i#define const' *.c *.h
.SQLite всё немного усложняет, с помощью скриптов генерируя код в ходе сборки. К счастью, компиляторы вносят много помех при смешивании кода с
const
и без const
, так что это можно было сразу заметить и настроить скрипты для добавления моего анти-const
кода.Прямое сравнение скомпилированных кодов не имеет смысла, поскольку мелкое изменение может повлиять на всю схему памяти, что приведёт к изменению указателей и вызовов функций во всём коде. Поэтому я снял дизассемблерный слепок (
objdump -d libSQLite3.so.0.8.6
) в виде размера бинарника и мнемонического названия каждой инструкции. Например, эта функция:000000000005d570 <SQLite3_blob_read>:
5d570: 4c 8d 05 59 a2 ff ff lea -0x5da7(%rip),%r8 # 577d0 <SQLite3BtreePayloadChecked>
5d577: e9 04 fe ff ff jmpq 5d380 <blobReadWrite>
5d57c: 0f 1f 40 00 nopl 0x0(%rax)
Превращается в:
SQLite3_blob_read 7lea 5jmpq 4nopl
При компилировании я не менял сборочные настройки SQLite.
Анализ скомпилированного кода
У libSQLite3.so версия с
const
занимала 4 740 704 байтов, примерно на 0,1 % больше версии без const
с её 4 736 712 байтами. В обоих случаях было экспортировано 1374 функции (не считая низкоуровневые вспомогательные функции в PLT), и у 13 были какие-нибудь различия в слепках.Некоторые изменения были связаны с хаком предварительной обработки. К примеру, вот одна из изменившихся функций (я убрал некоторые определения, характерные для SQLite):
#define LARGEST_INT64 (0xffffffff|(((int64_t)0x7fffffff)<<32))
#define SMALLEST_INT64 (((int64_t)-1) - LARGEST_INT64)
static int64_t doubleToInt64(double r){
/*
** Many compilers we encounter do not define constants for the
** minimum and maximum 64-bit integers, or they define them
** inconsistently. And many do not understand the "LL" notation.
** So we define our own static constants here using nothing
** larger than a 32-bit integer constant.
*/
static const int64_t maxInt = LARGEST_INT64;
static const int64_t minInt = SMALLEST_INT64;
if( r<=(double)minInt ){
return minInt;
}else if( r>=(double)maxInt ){
return maxInt;
}else{
return (int64_t)r;
}
}
Если убрать
const
, то эти константы превращаются в static
-переменные. Не понимаю, зачем кому-то, кого не волнуют const
, делать эти переменные static
. Если убрать и static
, и const
, то GCC снова будет считать их константами, и мы получим тот же результат. Из-за таких static const
переменных изменения в трёх функциях из тринадцати оказались ложными, но я не стал их исправлять.SQLite использует много глобальных переменных, и с этим связано большинство настоящих
const
-оптимизаций: вроде замены сравнения с переменной на сравнение с константой, или частичного отката цикла на один шаг (чтобы понять, какие были сделаны оптимизации, я воспользовался Radare). Несколько изменений не стоят упоминания. SQLite3ParseUri()
содержит 487 инструкций, но const
внёс лишь одно изменение: взял эти два сравнения:test %al, %al
je <SQLite3ParseUri+0x717>
cmp $0x23, %al
je <SQLite3ParseUri+0x717>
И поменял местами:
cmp $0x23, %al
je <SQLite3ParseUri+0x717>
test %al, %al
je <SQLite3ParseUri+0x717>
Бенчмарки
SQLite поставляется с регрессионным тестом для измерения производительности, и я сотни раз прогнал его для каждой версии кода, используя стандартные сборочные настройки SQLite. Длительность исполнения в секундах:
const |
Без const |
|
Минимум |
10,658 |
10,803 |
Медиана |
11,571 |
11,519 |
Максимум |
11,832 |
11,658 |
Среднее |
11,531 |
11,492 |
Лично я не вижу особой разницы. Я убрал
const
изо всей программы, так что если бы была заметная разница, то её было был легко заметить. Впрочем, если для вас крайне важна производительность, то вас может порадовать даже крошечное ускорение. Давайте проведём статистический анализ.Мне нравится использовать для таких задач тест Mann-Whitney U. Он аналогичен более известному тесту t, предназначенному для определения различий в группах, но более устойчив к сложным случайным вариациям, возникающим при измерении времени на компьютерах (из-за непредсказуемых переключений контекста, ошибок в страницах памяти и т.д.). Вот результат:
const | Без const | |
---|---|---|
N | 100 | 100 |
Средняя категория (Mean rank) | 121,38 | 79,62 |
Mann-Whitney U | 2912 | |
Z | -5,10 | |
2-sided p value | <10-6 |
|
Средняя разница HL |
-0,056 с. |
|
95-процентный доверительный интервал |
-0,077… -0,038 с. |
Тест U обнаружил статистически значимую разницу в производительности. Но — сюрприз! — быстрее оказалась версия без
const
, примерно на 60 мс, то есть на 0,5 %. Похоже, небольшое количество сделанных «оптимизаций» не стоили увеличения количества кода. Вряд ли const
активировал какие-нибудь большие оптимизации, вроде автовекторизации. Конечно, ваш пробег может зависеть от различных флагов в компиляторе, или от его версии, или от кодовой базы, или от чего-нибудь ещё. Но мне кажется, будет честным сказать, что если даже const
повысили производительность C, то я этого не заметил.Так для чего нужен const?
При всех его недостатках,
const
в C/C++ полезен для обеспечения типобезопасности. В частности, если применять const
в сочетании с move-семантикой и std::unique_pointer
, то можно реализовать явное владение указателем. Неопределённость владения указателем было огромной проблемой в старых кодовых базах на С++ размером свыше 100 KLOC, так что я благодарен const
за её решение.Однако раньше я выходил за рамки использования
const
для обеспечения типобезопасности. Я слышал, что считалась правильным как можно активнее применять const
ради повышения производительности. Я слышал, если производительность действительно важна, то нужно было рефакторить код, чтобы добавить побольше const
, даже если код становился менее читабельным. В то время это звучало разумно, но с тех пор я понял, что это неправда. Комментарии (74)
vanxant
23.08.2019 14:03-1const это просто сообщение другим программистам (и самому себе через N времени), что «я не собираюсь это менять в этой части программы». Оно совершенно не значит, что объект не будет изменён где-то или когда-то ещё.
Для второго случая (и в том числе для оптимизаций) в более других языках придумали final. Либо завезли const сразу со смыслом final.mayorovp
23.08.2019 14:16В тех контекстах, в которых final применимо, const тоже справляется.
Если искать что-то более мощное — надо смотреть в сторону контроля времени жизни и правил заимствования.
vanxant
23.08.2019 14:31+1В тех контекстах, в которых final применимо, const тоже справляется.
Гослинг с вами не согласен.
konshyn
23.08.2019 16:01Да можно и посмотрев код понять, что никто не собирался менять переменную.
Плюс константная ссылка ловит временные объекты и не нужна перегрузка & и &&.
А самый важный фактор, имхо, зачем использовать конст в некоторых местах — это то, что константные объекты доступны только на чтение и соответственно должны быть потокобезопасными.mayorovp
23.08.2019 16:18константные объекты доступны только на чтение и соответственно должны быть потокобезопасными
Вот только гарантий что этот объект не используется одновременно в другом месте для записи — нет.
konshyn
23.08.2019 16:27Это понятно. Поэтому и добавил про «некоторые места».
Ниже комментарий, в котором более лаконично описано то, что я хотел сказать: «const — это про семантику, а не оптимизацию»
monah_tuk
25.08.2019 01:52Вы, судя по всему, никогда не работали с библиотеками, от которых у вас только интерфейс и посмотреть в реализацию не получится.
monah_tuk
25.08.2019 01:54const correctness. Я тоже ни разу не использовал
const
в надежде на ускорение кода. Да и вообще, иммутабельность — это очень хорошо в контексте многопоточного программирования.
ice2heart
23.08.2019 15:03Я не уверен, но на простых примерах компилятор может вообще использовать результат функции.
#include <stdio.h> int func(const int num){ int result = 0; for (int i=0; i<num; ++i){ result += i; } return result; } int func2(int num){ int result = 0; for (int i=0; i<num; ++i){ result += i; } return result; } int main() { printf("%d, %d\n", func(10), func2(10)); return 0; }
выхлоп llvm
define i32 @main() local_unnamed_addr #1 {
%1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i64 0, i64 0), i32 45, i32 45)
ret i32 0
}
Хотя сами функции одинаковы. Возможно в более сложных примерах const позволяет чуть лучше оптимизировать.
Holix
23.08.2019 15:10+1Разве const не говорит компилятору помещать такие данные(инициализированные строки/массивы), объявленные как статические или глобальные, в Read-Only разделы памяти? На процессорах с Гарвардской архитектурой это очень важно.
mayorovp
23.08.2019 15:17+1Структуры с нетривиальным конструктором туда так просто не поместить.
И нет, const не может говорить компилятору ничего подобного, ведь стандарт не предписывает никакого единственно возможного способа генерации машинного кода, лишь устанавливает требования к поведению.
monah_tuk
25.08.2019 01:55И нет, const не может говорить компилятору ничего подобного
Ну почему? На конкретной платформе конкретная реализация компилятора может использовать подобное знание. Т.е. может — да, обязан — нет.
aamonster
23.08.2019 16:07Насколько я помню, в avr-gcc (типичный пример компилятора для гарвардской архитектуры) это не так, нужно явно указывать атрибут PROGMEM.
LennyB
23.08.2019 16:10Из драфта C11 (документ n1570):
132) The implementation may place a const object that is not volatile in a read-only region of storage. Moreover, the implementation need not allocate storage for such an object if its address is never used.
Может, но, видимо, не обязательно.
pavel_pimenov
23.08.2019 20:45habr.com/ru/company/infopulse/blog/322320
"… закончил серию изменений в коде браузера Chrome, которая уменьшила размер его бинарника под Windows примерно на 1 мегабайт, перенесла около 500 КB из read/write сегмента в read-only, а также уменьшила потребление оперативной памяти в общем примерно на 200 KB на каждый процесс Chrome. Удивительное заключается в том, что конкретно данная серия изменений состояла исключительно из удаления и добавления ключевого слова const в некоторых местах кода. Да, компиляторы — странные."
Ryppka
23.08.2019 15:24-2В C объявление
бессмысленно, как и константность возвращаемого значения, константность аргумента может иметь значение только в теле определения функции.void constFunc(const int *x);
C++, кстати, хоть и строже, но тоже может игнорировать константность возвращаемого значения и аргумента, но лень искать, где про это говориться явно.
Так что боюсь, все Ваши результаты про неизменность вызовов самоочевидны и без изучения ассемблера.Ryppka
23.08.2019 17:52Сорри, поспешил. Указатель на константу — это, конечно работает. В C нет смысла в константах-возвращаемых значениях в объявлениях и аргументах.
Ryppka
24.08.2019 12:54В помощь минусующим цитаты из руководств компиляторов.
Это из Интел по-поводу возвращаемых значений:
Compiler generates this warning when it finds a type qualifier applied to the return type of a function. C++ allows the type-qualifier to be const or volatile, but either qualifier applied to a function return type is meaningless, because functions can only return rvalues and the type qualifiers apply only to lvalues.
Аналогичные предупреждения делают и многие другие компиляторы, хотя и не все.
Ну и выдержка из стандарта по-поводу аргументов:
Parameter declarations that differ only in the presence or absence of const and/or volatile are equivalent. That is, the const and volatile type-specifiers for each parameter type are ignored [...]
Only the const and volatile type-specifiers at the outermost level of the parameter type specification are ignored in this fashion; const and volatile type-specifiers buried within a parameter type specification are significant and can be used to distinguish overloaded function declarations. [...]
Для ссылок и указателей на константы это, естественно значимо. Тут я дал маху и поторопился.TheCalligrapher
24.08.2019 17:36Цитата из Интел тут ни к чему — Интел и их документация не славятся (мягко говоря) хорошим знанием языка С++. Вот и та цитата, которую вы здесь привели — безграмотная чушь. Эту чушь еще можно было худо-бедно подогнать под стандарт в рамках старинного С++98, но сегодня эта безграмотная чушь — безнадежна.
Цитата из стандарта языка, которую вы привели, описывает процесс формирования типа функции для целей overloading. Ни больше не меньше. Это совершено узкая, изолированная и посторонняя тема, не имеющая никакого отношения к рассматриваемому вопросу. К чему вы здесь ее привели?
В общем же ни о каком "ignored" для квалификаторов параметров и возвращаемого значения в С++ речи быть не может — они ни в коем случае не ignored (!).
TheCalligrapher
24.08.2019 17:26Оба утверждения — не верны и являются популярной "пионэрской твердилкой".
Константность параметров, как вы сами сказали, влияет на семантику параметра в теле функции. В частности, такая константность активно и широко используется в coding standards, которые запрещают менять значения параметров внутри функции.
Константность возвращаемого значения действительно бессмысленна лишь для скалярных типов. Как только возвращаемое значение становится класс-типом эта константность тоже начинает влиять на семантику.
Ryppka
24.08.2019 20:29-1Ну понятно, что документация компилятора Intel для Вас — так, пустое словоблудие, ну что они там понимают)
Про возвращаемое значение: скалярность и нескалярность не имеет значения, важно лишь возвращаем мы значение или ссылку/указатель. Возвращяемое значение — всегда rvalue, и константность/волатильность тут применена не может. Если мы возвращаем ссылку/указатель, то квалификатор того, на что они указывают, используется, т.к. мы можем обращаться к объекту через указатель, который сам по себе является rvalue.
В прототипе функции то же самое: если там значение, то квалификатор никак не используется, прототипы U f(T i); и U f(T const i); эквивалентны. А вот в определении параметры — это объявление локальных переменных и квалификация учитывается компилятором.
Так что нет, руководство компилятора Intel право, а Вы ошибаетесь: скалярный тип или класс — неважно, роль играет только возврат/передача параметра по значению или по ссылке. Что и требовалось доказать.TheCalligrapher
25.08.2019 18:57Да, именно словоблудие! Причем на словоблудие это я/мы им указывали не раз и по своей инициативе, и в процессе оплаченных Интелом (!) же ревью их документации, но воз, как говорится, и ныне там. Но основная проблема, боюсь, не в интеловской документации, а лично в вашем неумении понимать прочитанное.
Про возвращаемое значение — совершенно неверно! Тот факт, что возвращаемое значение является rvalue абсолютно ничего не меняет. Зачем вы это повторяете? Еще раз: в языке С++ для rvalue класс-типов cv-квалификация совершенно полноценно применима и всегда была применима (!), как я уже ясно сказал выше. Вы же почему-то продолжаете твердить, что она якобы "тут применена не может". Что за чушь? Попробуйте, в конце концов, сами
std::string foo() { return ""; } const std::string bar() { return ""; } int main() { foo() = "ABC"; bar() = "ABC"; }
Замечаете разницу? То-то. А далее уже самостоятельно: вперед изучать свойства cv-квалификации в С++.
Что касается параметров, вы вдруг полезли рассказывать про эквивалентность прототипов. Это так, но к чему вдруг здесь прототипы? Прототип описывает внешнюю спецификацию функции и она действительно не меняется от cv-квалификации параметров. Но к делу это вообще никак не относится. Еще раз, как уже было ясно сказано выше: cv-квалификация параметра имеет абсолютно явный и однозначный эффект по отношению к семантике этого параметра внутри определения функции. Прототипы к этому никакого отношения не имеют. (P.S. В С++ нет "прототипов").
Ryppka
25.08.2019 19:36Насчет C++ убедили, у const std::string нет оператора присваивания с this. Хотя пример, мало сказать, искусственный, и скорее показывает «дырку» в логике языка. Я, действительно, последнее время больше имею дело с C, где такое невозможно, и там cv-квалификация возвращаемого значения не играет роли.
По поводу параметров и отсутствия прототипов функций в C++ — не убедили. Возможно, в standardize последних версий термина «прототип функции» и нет, но используется он в отношении C++ повсеместно.
a-tk
23.08.2019 15:25const — это про семантические ограничения кода, а не про кодогенерацию. Но иногда такие ограничения могут компилятору помочь.
v2kxyz
23.08.2019 15:57+1Я слышал, если производительность действительно важна, то нужно было рефакторить код, чтобы добавить побольше const, даже если код становился менее читабельным
А есть пример, где const как-то необратимо снижает читабельность?
Как и уже многие написали выше, я всегда считал, что const нужен больше как раз для читабельности и задания семантики. Писать на языке без констант — боль.
tmin10
23.08.2019 15:58+2Немного оффтопик, но я верно понимаю, что на КДПВ изображён С++, который хочет убить оптимизацию, но синтаксический сахар ему в этом мешает?
LennyB
23.08.2019 15:58+2Молодец автор, придумал миф и опроверг его.
mapron
24.08.2019 10:10Вы конечно, иронизируете, но что плохого в таком поступке по сути? Даже если и придумал — то своим исследованием возможно кого-то предупредил от такого же придумывания)
Вы ж не станете отрицать, что у кого-либо вообще может в голове возникнуть вопрос «а как, собственно, const может влиять на производительность?».
rus-blood
23.08.2019 16:16+1void constByArg(const int *x) { printf("%d\n", *x); constFunc(x); printf("%d\n", *x); }
Это означает только, что функция constByArg не изменяет значение по указателю x. Однако никто не гарантирует, что это значение не может быть изменено вызовом функции constFunc.
int x = 42; void constFunc(const int*) { x = 0; } int main() { constByArg(&x); }
monah_tuk
25.08.2019 02:04Да проще, данные не меняются, а вот сам указатель — вполне:
void constFunc(const int*& v) { v = new int(31337); }
нужно ещё больше
const
что бы от этого уйти ;-)
Laryx
23.08.2019 16:21По мне, главная польза от модификатора const — это ограничение прав. Он не дает случайно начать изменять переменные, которые не были предназначены для этого. Соответственно, главное увеличение эффективности лежит в области собственно программирования, а не исполнения.
А выигрыш в работе непосредственно на компьютере — и в самом деле, при хорошем компиляторе не должен быть сильно заметен.
GarryC
23.08.2019 17:14Почему const не ускоряет код на С/C++?
а что, должен был ускорять?WhiteBlackGoose
23.08.2019 18:02Лично я думал, что препроцессор компилятора заменяет все упоминания константы на ее значение, то есть если
const int a = 4;
…
b = a * 2
то для компилятора это будет
…
b =4 * 28Anton_Menshov
23.08.2019 18:20+3Вы путаете чем занимается препроцессор, а чем занимается компилятор. Препроцессор не разбирается с const. Он только заменит константы объявленные в #define стиле.
WhiteBlackGoose
23.08.2019 18:38-2Вот я и говорю, я думал, что это делает препроцессор. Кстати, а что ему мешает делать такую замену? Причем я имею ввиду не аргумент, разумеется, а там где грубо говоря человек может заменить. (как в моем примере выше)
mayorovp
23.08.2019 18:42+1Мешает ему это делать тот простой факт, что язык препроцессора — это чуть ли не отдельный язык программирования, и const не является в нём ключевом словом.
WhiteBlackGoose
23.08.2019 20:34Окей, понял. Тогда что мешает компилятору сделать то же самое? Я не сишник, помилуйте.
suVrik
23.08.2019 20:58+2Это как раз одна из оптимизаций компилятора. Называется constant propagation. Для неё const не обязателен.
mikkoljcov
24.08.2019 11:20Обычная путаница, учитывая что константные переменные (константы) и константы препроцессора называются одним словом. Легко запутаться.
TheCalligrapher
24.08.2019 17:41Путаница вызвана в первую очередь незнанием терминологии.
В языке С есть термин "константа". Константами в С называются буквальные значения (
1
,'a'
,3.14
) и элементы enum. Далее все покрывается термином "константное выражение".const
-объекты в C "константами" не называются вообще и константных выражений не формируют.
В языке С++ термина "константа" применяется только к элементам enum. Буквальные значения называются "литералами". Все остальное выражается через термин "константное выражение". Характерно то, что в С++, в отличие от С,
const
-объекты формируют константные выражения.
GarryC
26.08.2019 09:32Здесь не все так просто, в некоторых архитектурах (ARM) с константами очень плохо и поместить нетривиальную константу в регистр — это долго, так что в данном случае проще будет держать ее в близкой памяти.
Anton_Menshov
23.08.2019 17:22const действительно позволяет C-компилятору сгенерировать более эффективный код при использовании restrict.
TheCalligrapher
23.08.2019 17:47Какое-то очередное "открытие Америки".
Во-первых,
const
, разумеется, "ускоряет" код в тех ситуациях, когда он применяется к самому объекту, а не к "пути доступа" к объекту. Это прекрасно видно во всем, включая сгенерированный компилятором код. В применении же к путям доступаconst
действительно является лишь синтаксическим сахаром. Для оптимизации кода в таких ситуациях компилятору нужно знать полную картину aliasing, аconst
в этом никак не помогает. Для этого служитrestrict
.
Во-вторых, разглядывать для этих целей код, сгенерированный какими-то компиляторами — бессмысленно, особенно когда речь идет о GCC. GCC — отстал, крив и пионерск. Сегодня он не умеет что-то оптимизировать, но завтра — научится.
slonopotamus
23.08.2019 18:25+2разглядывать для этих целей код, сгенерированный какими-то компиляторами
Я извиняюсь, а код сгенерированный чем вместо компилятора вы предлагаете вместо этого рассматривать?TheCalligrapher
23.08.2019 22:15-1Я предлагаю рассматривать чисто теоретические соображения о том, какие оптимизации возможны, а какие — нет в рамках стандартной семантики языков С и С++. Если оптимизация теоретически возможна в рамках данного языка, то ее практическая реализация является лишь вопросом времени. То, что какая-то оптимизация еще не реализована неким компилятором, ничего не значит.
Не забывайте, что оптимизации в современных компиляторах С/С++ реализуются не на основе некоего "суперумного самообучающегося искусственного интеллекта", а на основе банальных механизмов pattern matching. Что будет и что не будет оптимизировать компилятор зависит только от того, какой набор оптимизационных паттернов в него уже успел вбить некий условный Вася Пупкин (в промежутках между сдачами сессий), и какой набор паттернов он НЕ успел вбить (в том числе потому, что бумажка с описанием соответствующего паттерна завалилась за шкаф).
Разглядывая сгенерированный компиляторами код вы фактически разглядываете последствия Васиного рабочего графика, Васиного энтузиазма или Васиной лени. Не надо на основе этого делать выводы об эффективности тех или иных конструкций языка. Особенно если учесть что репутация GCC сегодня лежит ниже плинтуса во многом из-за огромного баклога на фиксинг багов и реализацию критически необходимых фич.
slonopotamus
23.08.2019 23:55+3Мне кажется, вы в некоторой степени принижаете квалификацию людей, занимающихся разработкой компиляторов. Код GCC, возможно, далёк от идеала, но тем не менее в него вложено огромное количество человеколет, в течение которых производимые им бинарники совершенствовались и улучшались.
rogoz
23.08.2019 18:37Автору должны понравиться атрибуты __attribute__ ((const)) и __attribute__ ((pure)) в терминах GCC.
ktod
23.08.2019 19:07Странно, впервые слышу, что const — это про оптимизацию программы. Всегда считал, что const — это про оптимизацию процесса программирования.
DarkTiger
23.08.2019 20:07-1На мой взгляд (не претендую на абсолютное знание) эта байка про const пришла из операционок для микроконтроллеров ucLinux и иже с ним. Поскольку в микроконтроллерах размера флеша обычно хватает, а размера ОЗУ — нет, причем очень сильно не хватает, несколько килобайт — это слезы. Соответственно, переменные const кладутся во флеш, а не в ОЗУ. А чем больше остается свободного ОЗУ, тем быстрее станет доступна для выделения очередная страница памяти и тем быстрее работает система.
Причем, строго говоря, из флеша данные читаются медленнее, но ОЗУ мало, поэтому приходится жертвовать локальной оптимизацией во имя глобальной.
Zanak
23.08.2019 20:40Рискну предположить, что const является семантическим правилом, работающим на этапе анализа исходного кода, и на генерацию целевого кода, скорее всего, ни как не влияющим.
Пометка значения как константа — это скорее семантическое правило "любой код, который попытается изменить это значение — неверен" чем оптимизация (хотя, возможно, какой — то из компиляторов и умеет использовать это при генерации целевого кода).
ryo_oh_ki
23.08.2019 21:49Попробуйте:
void constFunc(const int * const x);
mayorovp
23.08.2019 22:52А в чём смысл? Компилятор и так знает что переменная x не изменяется.
andy_p
23.08.2019 23:30+1Ошибаетесь. Уберите второй const и попробуйте присвоить x какое- нибудь значение.
besitzeruf
24.08.2019 00:11это тип данных указатель на контантное число типа int (а значит не известо, ссылается ли указаль все на тот же участок памяти, особенно в embedded такое критично):
const int * x
это указатель, который низачто не изменит адрес, на который ссылается, но вот данные могут меняться:
int * const x
ну и как написали выше, это уже указатель, который гарантированно не изменит свой адрес и ссылается на НЕизменяемые данные:
const int * const x
mayorovp
24.08.2019 00:17а значит не известно, ссылается ли указатель все на тот же участок памяти, особенно в embedded такое критично
Вообще-то известно: если этой переменной ничего не присваивали, а ссылка на неё никуда не утекала, то ссылается.
AVI-crak
24.08.2019 01:20В том виде как используется const в этой статье — могут быть самые разнообразные результаты.
Но если привести реальный пример тотальной оптимизации, то условия будут такими:
1 Функция имеет в параметрах внешние константы — состояние которых известно компилятору. Это может быть обычные перемененные, но иницилизированные константами в пределах видимости вызова функции.
2 Функция выполняется один раз, либо функция определена как static inline. Это позволит разместить новый экземпляр функции в месте применения — сделав её одноразовой.
3 Результат вычислений должен иметь повторяемое практическое значение в общем алгоритме. В случае разного применения результата функции — оптимизация может сбойнуть.
Только в этом случае функция на константах полностью оптимизируется до одной записи результата вычислений. То-есть полностью выполняется препроцессором компилятора.
Функции на константах предназначены для замены многоэтажных зубодробительных макросов. Алгоритм в виде функции проще воспринимается и намного легче редактируется.
Функции на константах применяются в основном для смены формата имеющихся известных данных — посчитать один раз, и больше не мучится.
Yurec666
24.08.2019 01:28Автор, учите матчасть.
void foo(int *p) { // Needs to do more copying of data } void foo(const int *p) { // Doesn't need defensive copies } int main() { const int x = 42; // const-ness affects which overload gets called foo(&x); return 0; }
С одной стороны, я не думаю, что на практике это часто применяется в С++-коде.
Просто резануло глаза. Применяется очень часто.
poige
24.08.2019 09:01+2[хреновый перевод]
> Возможно, нам нужен компилятор поумнее. Скажем, Clang.
Не равнозначен исходному тексту:
> Maybe we just need a sufficiently smart compiler. Is Clang any better?
— В оригинале автор не утверждает, что «Clang поумнее», как это сделано в переводе. В оригинале автор интересуется: «окажется ли Clang чем-то лучше?»
Izaron
24.08.2019 10:16Почему-то не нашел в статье именно внятного ответа на вопрос "Почему" в заголовке. Давайте поиграем в телепатов и найдём правдоподобные объяснения для данного кейса с SQLite.
С языком Си понятно —
const
там просто обещание прогера не менять переменную после инициализацию, иначе UB будет. Что в С++:
Если в каком-нибудь примере заменить
const int a = 4
наint a = 4
, то переменная все равно останется "effectively const", потому что ее значение в коде не меняется, и "достаточно умный компилятор (тм)" должен по идее сам отловить, что переменная и есть константа (точнее, оба варианта должны работать одинаково), неважно какой код написан. Можно считатьconst
синтаксическим сахаром.
"По идее" const/read-only данные пихаются в другой сегмент данных, например,
.rdata
. Но в чем смысл это делать для одной автоматической переменной? Намного быстрее оставить переменную на стеке, как если бы это был не const. И там все остальные переменные рядом, не надо лазить туда-сюда по сегментам.
Antervis
24.08.2019 10:41const может влиять на перегрузки методов, эффективно ускоряя код, работающий с определенными типами данных
qrck13
24.08.2019 12:26Ускорять код const будет преимущественно тогда, когда на его месте по хорошему должен быть constexpr. Собственно зачем constexpr и был введен в C++. Ведь по сути const — это не гарантия того, что объект не может меняться. Это лишь аттрибут типа, по сути дела — часть контракта по использованию конкретно типа. Если в функцию передан указатель на const объект — функция не может его изменить.
А вот constexpr, с другой стороны, способствует массовым compile time оптимизациям, когда обращения к константам заменяются на inline значения в коде
TheCalligrapher
24.08.2019 17:46У вас наблюдается все та же путаница между двумя принципиально разными типами константности: константностью самого объекта и константностью пути доступа к объекту.
Константность объекта — это в С и С++ всегда гарантия того, что объект не может меняться (кроме
mutable
членов объектов классов).qrck13
24.08.2019 21:57Константность — это лишь гарантия что через данный конкретный "путь", как вы выразились, нельзя обьект поменять. Пример:
void doSomething(const std::string& str) { } ... std::string nonConstantString { "I am not a constant string" }; ... doSomething(nonConstantString);
в этом примере внутри "doSomething" строка видна только по "константному" пути, но сама "nonConstantString" не константна ни коем образом, более того значение в теории может поменяться даже пока вызов находится внутри "doSomething", так что никаких оптимизаций компилятор тут не будет иметь права сделать.
TheCalligrapher
25.08.2019 19:00Прекрасно, но к чему это здесь?
Еще раз: вы сделали утверждение, что "const — это не гарантия того, что объект не может меняться". Я вас поправляю:
const
верхнего уровня (т.е. примененный непосредственно к объекту, а не к пути доступа) является гарантией того, что объект не может меняться.
Ни больше, ни меньше.
qrck13
25.08.2019 19:29Прекрасно, но к чему это здесь?
Точно такой же вопрос можно задать на ваш первый ответ мне.
Yurec666
27.08.2019 03:19Я вас поправляю: const верхнего уровня (т.е. примененный непосредственно к объекту, а не к пути доступа) является гарантией того, что объект не может меняться
.
К сожалению это не совсем так, есть ведь const_cast. Разработчики стандарта давно говорят что это очень плохой механизм, но убрать его нельзя потому что половину кода придеться редезайнить
Ciberst
25.08.2019 17:18Хорошая статья, которая показывает, как компиляторы могут оптимизировать код?!
staticmain
И зачем, если есть -D?