Несколько месяцев назад я упомянул в одном посте, что это миф, будто бы 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 почти бесполезен для оптимизации, потому что:

  1. За несколькими исключениями, компилятор вынужден игнорировать его, поскольку какой-нибудь код может на законных основаниях отвязать const.
  2. В большинстве вышеупомянутых исключений компилятор всё-равно может понять, что переменная является константой.

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)


  1. staticmain
    23.08.2019 13:40
    +5

    (GNU) sed может добавить это поверх каждого файла с помощью команды sed -i '1i#define const' *.c *.h.

    И зачем, если есть -D?


  1. vanxant
    23.08.2019 14:03
    -1

    const это просто сообщение другим программистам (и самому себе через N времени), что «я не собираюсь это менять в этой части программы». Оно совершенно не значит, что объект не будет изменён где-то или когда-то ещё.
    Для второго случая (и в том числе для оптимизаций) в более других языках придумали final. Либо завезли const сразу со смыслом final.


    1. mayorovp
      23.08.2019 14:16

      В тех контекстах, в которых final применимо, const тоже справляется.


      Если искать что-то более мощное — надо смотреть в сторону контроля времени жизни и правил заимствования.


      1. vanxant
        23.08.2019 14:31
        +1

        В тех контекстах, в которых final применимо, const тоже справляется.

        Гослинг с вами не согласен.


    1. konshyn
      23.08.2019 16:01

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

      А самый важный фактор, имхо, зачем использовать конст в некоторых местах — это то, что константные объекты доступны только на чтение и соответственно должны быть потокобезопасными.


      1. mayorovp
        23.08.2019 16:18

        константные объекты доступны только на чтение и соответственно должны быть потокобезопасными

        Вот только гарантий что этот объект не используется одновременно в другом месте для записи — нет.


        1. konshyn
          23.08.2019 16:27

          Это понятно. Поэтому и добавил про «некоторые места».
          Ниже комментарий, в котором более лаконично описано то, что я хотел сказать: «const — это про семантику, а не оптимизацию»


      1. monah_tuk
        25.08.2019 01:52

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


    1. monah_tuk
      25.08.2019 01:54

      const correctness. Я тоже ни разу не использовал const в надежде на ускорение кода. Да и вообще, иммутабельность — это очень хорошо в контексте многопоточного программирования.


  1. 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 позволяет чуть лучше оптимизировать.


  1. Holix
    23.08.2019 15:10
    +1

    Разве const не говорит компилятору помещать такие данные(инициализированные строки/массивы), объявленные как статические или глобальные, в Read-Only разделы памяти? На процессорах с Гарвардской архитектурой это очень важно.


    1. mayorovp
      23.08.2019 15:17
      +1

      Структуры с нетривиальным конструктором туда так просто не поместить.


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


      1. monah_tuk
        25.08.2019 01:55

        И нет, const не может говорить компилятору ничего подобного

        Ну почему? На конкретной платформе конкретная реализация компилятора может использовать подобное знание. Т.е. может — да, обязан — нет.


    1. aamonster
      23.08.2019 16:07

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


    1. 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.

      Может, но, видимо, не обязательно.


    1. pavel_pimenov
      23.08.2019 20:45

      habr.com/ru/company/infopulse/blog/322320
      "… закончил серию изменений в коде браузера Chrome, которая уменьшила размер его бинарника под Windows примерно на 1 мегабайт, перенесла около 500 КB из read/write сегмента в read-only, а также уменьшила потребление оперативной памяти в общем примерно на 200 KB на каждый процесс Chrome. Удивительное заключается в том, что конкретно данная серия изменений состояла исключительно из удаления и добавления ключевого слова const в некоторых местах кода. Да, компиляторы — странные."


      1. perfect_genius
        24.08.2019 08:51

        Т.е. зато есть оптимизация по размеру?


  1. Ryppka
    23.08.2019 15:24
    -2

    В C объявление

    void constFunc(const int *x);
    бессмысленно, как и константность возвращаемого значения, константность аргумента может иметь значение только в теле определения функции.
    C++, кстати, хоть и строже, но тоже может игнорировать константность возвращаемого значения и аргумента, но лень искать, где про это говориться явно.
    Так что боюсь, все Ваши результаты про неизменность вызовов самоочевидны и без изучения ассемблера.


    1. Ryppka
      23.08.2019 17:52

      Сорри, поспешил. Указатель на константу — это, конечно работает. В C нет смысла в константах-возвращаемых значениях в объявлениях и аргументах.


      1. 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. [...]

        Для ссылок и указателей на константы это, естественно значимо. Тут я дал маху и поторопился.


        1. TheCalligrapher
          24.08.2019 17:36

          Цитата из Интел тут ни к чему — Интел и их документация не славятся (мягко говоря) хорошим знанием языка С++. Вот и та цитата, которую вы здесь привели — безграмотная чушь. Эту чушь еще можно было худо-бедно подогнать под стандарт в рамках старинного С++98, но сегодня эта безграмотная чушь — безнадежна.


          Цитата из стандарта языка, которую вы привели, описывает процесс формирования типа функции для целей overloading. Ни больше не меньше. Это совершено узкая, изолированная и посторонняя тема, не имеющая никакого отношения к рассматриваемому вопросу. К чему вы здесь ее привели?


          В общем же ни о каком "ignored" для квалификаторов параметров и возвращаемого значения в С++ речи быть не может — они ни в коем случае не ignored (!).


      1. TheCalligrapher
        24.08.2019 17:26

        Оба утверждения — не верны и являются популярной "пионэрской твердилкой".


        Константность параметров, как вы сами сказали, влияет на семантику параметра в теле функции. В частности, такая константность активно и широко используется в coding standards, которые запрещают менять значения параметров внутри функции.


        Константность возвращаемого значения действительно бессмысленна лишь для скалярных типов. Как только возвращаемое значение становится класс-типом эта константность тоже начинает влиять на семантику.


        1. Ryppka
          24.08.2019 20:29
          -1

          Ну понятно, что документация компилятора Intel для Вас — так, пустое словоблудие, ну что они там понимают)

          Про возвращаемое значение: скалярность и нескалярность не имеет значения, важно лишь возвращаем мы значение или ссылку/указатель. Возвращяемое значение — всегда rvalue, и константность/волатильность тут применена не может. Если мы возвращаем ссылку/указатель, то квалификатор того, на что они указывают, используется, т.к. мы можем обращаться к объекту через указатель, который сам по себе является rvalue.
          В прототипе функции то же самое: если там значение, то квалификатор никак не используется, прототипы U f(T i); и U f(T const i); эквивалентны. А вот в определении параметры — это объявление локальных переменных и квалификация учитывается компилятором.
          Так что нет, руководство компилятора Intel право, а Вы ошибаетесь: скалярный тип или класс — неважно, роль играет только возврат/передача параметра по значению или по ссылке. Что и требовалось доказать.


          1. 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. В С++ нет "прототипов").


            1. Ryppka
              25.08.2019 19:36

              Насчет C++ убедили, у const std::string нет оператора присваивания с this. Хотя пример, мало сказать, искусственный, и скорее показывает «дырку» в логике языка. Я, действительно, последнее время больше имею дело с C, где такое невозможно, и там cv-квалификация возвращаемого значения не играет роли.
              По поводу параметров и отсутствия прототипов функций в C++ — не убедили. Возможно, в standardize последних версий термина «прототип функции» и нет, но используется он в отношении C++ повсеместно.


  1. a-tk
    23.08.2019 15:25

    const — это про семантические ограничения кода, а не про кодогенерацию. Но иногда такие ограничения могут компилятору помочь.


  1. amarao
    23.08.2019 15:29
    -5

    const в C/C++ нужен для увеличения числа случаев, когда возникает UB.


    1. monah_tuk
      25.08.2019 02:00

      Так наоборот же! :)


  1. v2kxyz
    23.08.2019 15:57
    +1

    Я слышал, если производительность действительно важна, то нужно было рефакторить код, чтобы добавить побольше const, даже если код становился менее читабельным

    А есть пример, где const как-то необратимо снижает читабельность?
    Как и уже многие написали выше, я всегда считал, что const нужен больше как раз для читабельности и задания семантики. Писать на языке без констант — боль.


  1. tmin10
    23.08.2019 15:58
    +2

    Немного оффтопик, но я верно понимаю, что на КДПВ изображён С++, который хочет убить оптимизацию, но синтаксический сахар ему в этом мешает?


  1. LennyB
    23.08.2019 15:58
    +2

    Молодец автор, придумал миф и опроверг его.


    1. mapron
      24.08.2019 10:10

      Вы конечно, иронизируете, но что плохого в таком поступке по сути? Даже если и придумал — то своим исследованием возможно кого-то предупредил от такого же придумывания)
      Вы ж не станете отрицать, что у кого-либо вообще может в голове возникнуть вопрос «а как, собственно, const может влиять на производительность?».


  1. rus-blood
    23.08.2019 16:16
    +1

    void 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);
    }
    


    1. monah_tuk
      25.08.2019 02:04

      Да проще, данные не меняются, а вот сам указатель — вполне:


      void constFunc(const int*& v) 
      {
        v = new int(31337);
      }

      нужно ещё больше const что бы от этого уйти ;-)


  1. Laryx
    23.08.2019 16:21

    По мне, главная польза от модификатора const — это ограничение прав. Он не дает случайно начать изменять переменные, которые не были предназначены для этого. Соответственно, главное увеличение эффективности лежит в области собственно программирования, а не исполнения.

    А выигрыш в работе непосредственно на компьютере — и в самом деле, при хорошем компиляторе не должен быть сильно заметен.


  1. GarryC
    23.08.2019 17:14

    Почему const не ускоряет код на С/C++?
    а что, должен был ускорять?


    1. WhiteBlackGoose
      23.08.2019 18:02

      Лично я думал, что препроцессор компилятора заменяет все упоминания константы на ее значение, то есть если
      const int a = 4;

      b = a * 2


      то для компилятора это будет

      b = 4 * 2 8


      1. Anton_Menshov
        23.08.2019 18:20
        +3

        Вы путаете чем занимается препроцессор, а чем занимается компилятор. Препроцессор не разбирается с const. Он только заменит константы объявленные в #define стиле.


        1. WhiteBlackGoose
          23.08.2019 18:38
          -2

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


          1. mayorovp
            23.08.2019 18:42
            +1

            Мешает ему это делать тот простой факт, что язык препроцессора — это чуть ли не отдельный язык программирования, и const не является в нём ключевом словом.


            1. WhiteBlackGoose
              23.08.2019 20:34

              Окей, понял. Тогда что мешает компилятору сделать то же самое? Я не сишник, помилуйте.


              1. suVrik
                23.08.2019 20:58
                +2

                Это как раз одна из оптимизаций компилятора. Называется constant propagation. Для неё const не обязателен.


                1. assembled
                  23.08.2019 21:39

                  Ага.

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


        1. mikkoljcov
          24.08.2019 11:20

          Обычная путаница, учитывая что константные переменные (константы) и константы препроцессора называются одним словом. Легко запутаться.


          1. TheCalligrapher
            24.08.2019 17:41

            Путаница вызвана в первую очередь незнанием терминологии.


            В языке С есть термин "константа". Константами в С называются буквальные значения (1, 'a', 3.14) и элементы enum. Далее все покрывается термином "константное выражение". const-объекты в C "константами" не называются вообще и константных выражений не формируют.


            В языке С++ термина "константа" применяется только к элементам enum. Буквальные значения называются "литералами". Все остальное выражается через термин "константное выражение". Характерно то, что в С++, в отличие от С, const-объекты формируют константные выражения.


      1. GarryC
        26.08.2019 09:32

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


  1. Anton_Menshov
    23.08.2019 17:22

    const действительно позволяет C-компилятору сгенерировать более эффективный код при использовании restrict.


  1. TheCalligrapher
    23.08.2019 17:47

    Какое-то очередное "открытие Америки".


    Во-первых, const, разумеется, "ускоряет" код в тех ситуациях, когда он применяется к самому объекту, а не к "пути доступа" к объекту. Это прекрасно видно во всем, включая сгенерированный компилятором код. В применении же к путям доступа const действительно является лишь синтаксическим сахаром. Для оптимизации кода в таких ситуациях компилятору нужно знать полную картину aliasing, а const в этом никак не помогает. Для этого служит restrict.


    Во-вторых, разглядывать для этих целей код, сгенерированный какими-то компиляторами — бессмысленно, особенно когда речь идет о GCC. GCC — отстал, крив и пионерск. Сегодня он не умеет что-то оптимизировать, но завтра — научится.


    1. slonopotamus
      23.08.2019 18:25
      +2

      разглядывать для этих целей код, сгенерированный какими-то компиляторами

      Я извиняюсь, а код сгенерированный чем вместо компилятора вы предлагаете вместо этого рассматривать?


      1. TheCalligrapher
        23.08.2019 22:15
        -1

        Я предлагаю рассматривать чисто теоретические соображения о том, какие оптимизации возможны, а какие — нет в рамках стандартной семантики языков С и С++. Если оптимизация теоретически возможна в рамках данного языка, то ее практическая реализация является лишь вопросом времени. То, что какая-то оптимизация еще не реализована неким компилятором, ничего не значит.


        Не забывайте, что оптимизации в современных компиляторах С/С++ реализуются не на основе некоего "суперумного самообучающегося искусственного интеллекта", а на основе банальных механизмов pattern matching. Что будет и что не будет оптимизировать компилятор зависит только от того, какой набор оптимизационных паттернов в него уже успел вбить некий условный Вася Пупкин (в промежутках между сдачами сессий), и какой набор паттернов он НЕ успел вбить (в том числе потому, что бумажка с описанием соответствующего паттерна завалилась за шкаф).


        Разглядывая сгенерированный компиляторами код вы фактически разглядываете последствия Васиного рабочего графика, Васиного энтузиазма или Васиной лени. Не надо на основе этого делать выводы об эффективности тех или иных конструкций языка. Особенно если учесть что репутация GCC сегодня лежит ниже плинтуса во многом из-за огромного баклога на фиксинг багов и реализацию критически необходимых фич.


        1. slonopotamus
          23.08.2019 23:55
          +3

          Мне кажется, вы в некоторой степени принижаете квалификацию людей, занимающихся разработкой компиляторов. Код GCC, возможно, далёк от идеала, но тем не менее в него вложено огромное количество человеколет, в течение которых производимые им бинарники совершенствовались и улучшались.


  1. yleo
    23.08.2019 18:32

    Ерундовая статья.
    Сходите к Косте Осипову, если писать не о чем.


  1. rogoz
    23.08.2019 18:37

    Автору должны понравиться атрибуты __attribute__ ((const)) и __attribute__ ((pure)) в терминах GCC.


  1. ktod
    23.08.2019 19:07

    Странно, впервые слышу, что const — это про оптимизацию программы. Всегда считал, что const — это про оптимизацию процесса программирования.


  1. DarkTiger
    23.08.2019 20:07
    -1

    На мой взгляд (не претендую на абсолютное знание) эта байка про const пришла из операционок для микроконтроллеров ucLinux и иже с ним. Поскольку в микроконтроллерах размера флеша обычно хватает, а размера ОЗУ — нет, причем очень сильно не хватает, несколько килобайт — это слезы. Соответственно, переменные const кладутся во флеш, а не в ОЗУ. А чем больше остается свободного ОЗУ, тем быстрее станет доступна для выделения очередная страница памяти и тем быстрее работает система.
    Причем, строго говоря, из флеша данные читаются медленнее, но ОЗУ мало, поэтому приходится жертвовать локальной оптимизацией во имя глобальной.


  1. phantom-code
    23.08.2019 20:27

    Я просто оставлю это здесь: Why Const Sucks


  1. Zanak
    23.08.2019 20:40

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


  1. ryo_oh_ki
    23.08.2019 21:49

    Попробуйте:

    void constFunc(const int * const x);


    1. mayorovp
      23.08.2019 22:52

      А в чём смысл? Компилятор и так знает что переменная x не изменяется.


      1. andy_p
        23.08.2019 23:30
        +1

        Ошибаетесь. Уберите второй const и попробуйте присвоить x какое- нибудь значение.


      1. besitzeruf
        24.08.2019 00:11

        это тип данных указатель на контантное число типа int (а значит не известо, ссылается ли указаль все на тот же участок памяти, особенно в embedded такое критично):

        const int * x


        это указатель, который низачто не изменит адрес, на который ссылается, но вот данные могут меняться:
        int * const  x


        ну и как написали выше, это уже указатель, который гарантированно не изменит свой адрес и ссылается на НЕизменяемые данные:
        const int * const  x


        1. mayorovp
          24.08.2019 00:17

          а значит не известно, ссылается ли указатель все на тот же участок памяти, особенно в embedded такое критично

          Вообще-то известно: если этой переменной ничего не присваивали, а ссылка на неё никуда не утекала, то ссылается.


  1. AVI-crak
    24.08.2019 01:20

    В том виде как используется const в этой статье — могут быть самые разнообразные результаты.
    Но если привести реальный пример тотальной оптимизации, то условия будут такими:
    1 Функция имеет в параметрах внешние константы — состояние которых известно компилятору. Это может быть обычные перемененные, но иницилизированные константами в пределах видимости вызова функции.
    2 Функция выполняется один раз, либо функция определена как static inline. Это позволит разместить новый экземпляр функции в месте применения — сделав её одноразовой.
    3 Результат вычислений должен иметь повторяемое практическое значение в общем алгоритме. В случае разного применения результата функции — оптимизация может сбойнуть.

    Только в этом случае функция на константах полностью оптимизируется до одной записи результата вычислений. То-есть полностью выполняется препроцессором компилятора.

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


  1. 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;
    }

    С одной стороны, я не думаю, что на практике это часто применяется в С++-коде.

    Просто резануло глаза. Применяется очень часто.


  1. poige
    24.08.2019 09:01
    +2

    [хреновый перевод]

    > Возможно, нам нужен компилятор поумнее. Скажем, Clang.

    Не равнозначен исходному тексту:

    > Maybe we just need a sufficiently smart compiler. Is Clang any better?

    — В оригинале автор не утверждает, что «Clang поумнее», как это сделано в переводе. В оригинале автор интересуется: «окажется ли Clang чем-то лучше?»


  1. Izaron
    24.08.2019 10:16

    Почему-то не нашел в статье именно внятного ответа на вопрос "Почему" в заголовке. Давайте поиграем в телепатов и найдём правдоподобные объяснения для данного кейса с SQLite.


    С языком Си понятно — const там просто обещание прогера не менять переменную после инициализацию, иначе UB будет. Что в С++:


    1. Если в каком-нибудь примере заменить const int a = 4 на int a = 4, то переменная все равно останется "effectively const", потому что ее значение в коде не меняется, и "достаточно умный компилятор (тм)" должен по идее сам отловить, что переменная и есть константа (точнее, оба варианта должны работать одинаково), неважно какой код написан. Можно считать const синтаксическим сахаром.


    2. "По идее" const/read-only данные пихаются в другой сегмент данных, например, .rdata. Но в чем смысл это делать для одной автоматической переменной? Намного быстрее оставить переменную на стеке, как если бы это был не const. И там все остальные переменные рядом, не надо лазить туда-сюда по сегментам.



  1. Antervis
    24.08.2019 10:41

    const может влиять на перегрузки методов, эффективно ускоряя код, работающий с определенными типами данных


  1. qrck13
    24.08.2019 12:26

    Ускорять код const будет преимущественно тогда, когда на его месте по хорошему должен быть constexpr. Собственно зачем constexpr и был введен в C++. Ведь по сути const — это не гарантия того, что объект не может меняться. Это лишь аттрибут типа, по сути дела — часть контракта по использованию конкретно типа. Если в функцию передан указатель на const объект — функция не может его изменить.


    А вот constexpr, с другой стороны, способствует массовым compile time оптимизациям, когда обращения к константам заменяются на inline значения в коде


    1. TheCalligrapher
      24.08.2019 17:46

      У вас наблюдается все та же путаница между двумя принципиально разными типами константности: константностью самого объекта и константностью пути доступа к объекту.


      Константность объекта — это в С и С++ всегда гарантия того, что объект не может меняться (кроме mutable членов объектов классов).


      1. 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", так что никаких оптимизаций компилятор тут не будет иметь права сделать.


        1. TheCalligrapher
          25.08.2019 19:00

          Прекрасно, но к чему это здесь?


          Еще раз: вы сделали утверждение, что "const — это не гарантия того, что объект не может меняться". Я вас поправляю: const верхнего уровня (т.е. примененный непосредственно к объекту, а не к пути доступа) является гарантией того, что объект не может меняться.


          Ни больше, ни меньше.


          1. qrck13
            25.08.2019 19:29

            Прекрасно, но к чему это здесь?

            Точно такой же вопрос можно задать на ваш первый ответ мне.


          1. Yurec666
            27.08.2019 03:19

            Я вас поправляю: const верхнего уровня (т.е. примененный непосредственно к объекту, а не к пути доступа) является гарантией того, что объект не может меняться
            .
            К сожалению это не совсем так, есть ведь const_cast. Разработчики стандарта давно говорят что это очень плохой механизм, но убрать его нельзя потому что половину кода придеться редезайнить


  1. Ciberst
    25.08.2019 17:18

    Хорошая статья, которая показывает, как компиляторы могут оптимизировать код?!