Должна ли инициализация локальной переменной быть обязательной?

Проблемы с обслуживанием, которые вызывают неинициализированные локальные переменные (особенно указатели), будут очевидны для любого, кто немного занимался обслуживанием или улучшением c / C++, но я все еще вижу их и иногда слышу о последствиях для производительности в качестве их оправдания.

В c легко продемонстрировать, что избыточная инициализация оптимизирована:

$ less test.c
#include <stdio.h>
main()
{
#ifdef INIT_LOC
    int a = 33;
    int b;
    memset(&b,66,sizeof(b));
#else
    int a;
    int b;
#endif
    a = 0;
    b = 0;
    printf ("a = %i, b = %i\n", a, b);
}

$ gcc --version
gcc (GCC) 3.4.4 (cygming special, gdc 0.12, using dmd 0.125)

[Не оптимизировано:]

$ gcc test.c -S -o no_init.s; gcc test.c -S -D INIT_LOC=1 -o init.s; diff no_in
it.s init.s
22a23,28
>       movl    , -4(%ebp)
>       movl    , 8(%esp)
>       movl    , 4(%esp)
>       leal    -8(%ebp), %eax
>       movl    %eax, (%esp)
>       call    _memset
33a40
>       .def    _memset;        .scl    3;      .type   32;     .endef

[Оптимизировано:]

$ gcc test.c -O -S -o no_init.s; gcc test.c -O -S -D INIT_LOC=1 -o init.s; diff
 no_init.s init.s
$

Итак, производительность WRT, при каких обстоятельствах обязательная инициализация переменной НЕ является хорошей идеей?

ЕСЛИ применимо, нет необходимости ограничивать ответы на c / C++, но, пожалуйста, четко укажите язык / среду (и воспроизводимые доказательства предпочтительнее предположений!)

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
11
0
2 640
17

Ответы 17

Иногда вам нужна переменная в качестве заполнителя (например, при использовании функций ftime), поэтому нет смысла инициализировать их перед вызовом функции инициализации.

Однако, на мой взгляд, было бы неплохо отметить тот факт, что вы знаете о подводных камнях, что-то на пути

uninitialized time_t t;
time( &t );

Если бы вы сделали это обязательным, вы могли бы просто сделать это, что, на мой взгляд, имеет не меньший смысл: time_t t = {0};

schmick 28.09.2008 17:41

Я не уверен, нужно ли «делать их обязательными», но лично я считаю, что всегда лучше инициализировать переменные. Если цель приложения - быть как можно более тесной, то для этой цели открыт C / C++. Однако я думаю, что многие из нас раз или два сгорели из-за того, что не инициализировали переменную и предполагали, что она содержит допустимое значение (например, указатель), хотя на самом деле это не так. Указатель с нулевым адресом намного легче проверить, чем наличие случайного мусора из последнего содержимого памяти в этом конкретном месте. Я думаю, что в большинстве случаев это уже не вопрос производительности, а вопрос ясности и безопасности.

Краткий ответ: объявите переменную как можно ближе к первому использованию и инициализируйте ее значением «ноль», если вам все еще нужно.

Длинный ответ: если вы объявляете переменную в начале функции и не используете ее позже, вам следует пересмотреть свое размещение переменной в максимально локальной области видимости. Затем вы можете сразу же присвоить ему необходимое значение.

Если вы должны объявить его неинициализированным, потому что он назначается в условном выражении или передается по ссылке и назначается, инициализация его значением, эквивалентным нулю, является хорошей идеей. Компилятор иногда может спасти вас, если вы компилируете под -Wall, так как он предупредит вас, если вы прочитаете переменную перед ее инициализацией. Однако он не предупреждает вас, если вы передадите его функции.

Если вы перестрахуетесь и установите для него нулевой эквивалент, вы не причините вреда, если функция, которой вы передадите его, перезапишет его. Если, однако, функция, которой вы ее передаете, использует значение, вы можете в значительной степени гарантировать, что не будет выполнено утверждение (если оно у вас есть) или, по крайней мере, произойдет отказ во втором случае, когда вы используете нулевой объект. Случайная инициализация может привести к разного рода плохим вещам, включая «работу».

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

Dima 26.09.2008 18:12

общий случай переменной, объявленной перед циклом / условной структурой, поэтому вы можете установить значение в «цикле» и сохранить значение впоследствии

ShoeLace 26.09.2008 18:12

вы получите предупреждение с параметром -Wall, только если вы читаете переменную, в которую никогда не писали. Это отличается от кодирования в стиле «инициализировать на всякий случай».

Nils Pipenbrinck 26.09.2008 18:14

Спасибо за комментарии; Я это починил. Надо было протестировать -Wall :(

hazzen 26.09.2008 19:17

@ShoeLace: Итак, если цикл вообще не повторяется, каково значение вашей переменной? Я бы сказал, что это именно та вещь, которую «всегда инициализировать» помогает предотвратить / выделить.

Richard Corden 26.09.2008 19:22

@H Ваши короткие и длинные не учитывают, что то, что вы предлагаете, невозможно в C, и даже если бы это было так, они не предполагают никаких проблем с производительностью при инициализации всего. Кроме того, я не вижу, как передача инициализированной переменной в функцию может быть чем угодно, но не ЛУЧШЕ, чем неопределенная?

schmick 28.09.2008 18:47

Представление? Настоящее время? Возможно, когда процессоры работали на частоте 10 МГц, это имело смысл, но сегодня это вряд ли проблема. Всегда инициализируйте их.

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

Nils Pipenbrinck 26.09.2008 18:15

Есть также люди, которые пишут тяжелый код для обработки научных чисел, который работает в течение нескольких дней / недель. Даже улучшение на 1% является значительным. Но я согласен, что ваш компилятор должен быть настроен так, чтобы по умолчанию отмечать это как предупреждение.

KeithB 26.09.2008 18:41

Да уж, именно такие мысли делают наши современные компьютеры такими же медленными, как и старые. Новые программисты пишут раздутый код с менталитетом «теперь процессор работает быстро, мы можем себе это позволить».

Evan Teran 12.11.2008 08:57

Да, эм, я разрабатываю изрядное количество кода для коробки с частотой 40 МГц. Возможно, мне придется перенести его на коробку с частотой 8 МГц. Пожалуйста, не будьте небрежны в исполнении.

Paul Nathan 13.11.2008 01:32

Нет, инициализация переменных не снижает производительность. Для этого вам нужно рассматривать библиотеки / языки, которые имеют копии памяти при любой возможности как функцию. Не оптимизируйте преждевременно, инициализация переменных не будет вашей проблемой.

gbjbaanb 14.11.2008 02:52

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

Тем не менее: если вы инициализируете материал простыми операторами, просто чтобы убедиться, что он инициализирован, это нормально ... Я лично не делаю этого по одной причине:

Это заставляет ребят, которые позже могут поддерживать ваш код, думать, что инициализация необходима. Этот маленький foo = 0; увеличит сложность кода. В остальном это дело вкуса.

Если вы необоснованно инициализируете переменные с помощью сложных операторов, это может иметь побочный эффект.

Например:

  float x = sqrt(0);

Может быть оптимизирован вашим компилятором, если вам повезет и вы работаете с умным компилятором. С не очень умным компилятором это может также привести к дорогостоящему и бессмысленному вызову функции, потому что sqrt может - в качестве побочного эффекта - установить переменную errno.

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

В C / C++ полностью с вами согласен.

В Perl, когда я создаю переменную, ей автоматически присваивается значение по умолчанию.

my ($val1, $val2, $val3, $val4);
print $val1, "\n";
print $val1 + 1, "\n";
print $val2 + 2, "\n";
print $val3 = $val3 . 'Hello, SO!', "\n";
print ++$val4 +4, "\n";

Все они изначально настроены на undef. Undef - это ложное значение и заполнитель. Из-за динамической типизации, если я добавляю к ней число, он предполагает, что моя переменная является числом, и заменяет undef эквивалентным ложным значением 0. Если я выполняю строковые операции, ложная версия строки является пустой строкой, и это получает автоматически подставляется.

[jeremy@localhost Code]$ ./undef.pl

1
2
Hello, SO!
5

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

 my($x, $y, $z);

:-)

 my $x = 0;
 my $y = 0;
 my $z = 0;

Это отличный пример Преждевременная оптимизация - корень всех зол

Полная цитата:

There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified.

Это пришло из Дональд Кнут. Кому вы поверите ... своим коллегам или Кнуту?
Я знаю, где мои деньги ...

Чтобы вернуться к исходному вопросу: «Должны ли мы ОБЯЗАТЕЛЬНО инициализировать?»
Я бы сформулировал это так:

Variables should be initialize, except in situation where it can be demonstrated there is a significant performance gain to be realized by not initializing. Come armed with hard numbers...

Дональд Кнут насмехается над параллельным программированием и многопоточностью. informit.com/articles/article.aspx?p=1193856

J.J. 26.09.2008 18:24

Я ненавижу, когда люди цитируют только половину предложения и вырывают это из контекста, кстати ...

Nils Pipenbrinck 26.09.2008 18:26

Не отменяет его точку зрения ... Я не думаю, что инициализация var - это критические 3%

Benoit 26.09.2008 18:28

Проблема здесь не в производительности, а в использовании. Вы не хотите использовать (читать) переменную, которая еще не была инициализирована.

Herms 26.09.2008 18:35

@JJ Интересная статья Спасибо! Я думаю, что насмешки немного сильны ... может быть лучше с здравым смыслом :) Обратите внимание, что он признает свое невежество по этому поводу и говорит, не слушайте меня ...

Benoit 26.09.2008 18:39

@Herms: в вопросе говорится, что `` влияние на производительность приводится как оправдание ''. Это вопрос, к которому я обращаюсь

Benoit 26.09.2008 18:40

@B Спасибо, и я, конечно, согласен (хотя я не мог указать источник, мне в голову пришла та же цитата). Однако я хочу знать (по соображениям производительности), при каких обстоятельствах (если таковые имеются) НЕ стоит просто ЗАПРЕТИТЬ инициализировать локальную переменную.

schmick 28.09.2008 17:16

@schmick Когда вы можете продемонстрировать, это означает значительную потерю производительности. Часто приходят на ум называемые тугие петли. Я БУДУ БЫ ОБЯЗАТЕЛЬНЫМ, но с исключением, если вы можете ДЕМОНСТРИРОВАТЬ

Benoit 28.09.2008 19:10

Иногда переменная используется для «сбора» результата более длинного блока вложенных ifs / elses ... В таких случаях я иногда оставляю переменную неинициализированной, потому что она должен будет инициализирована позже одной из условных ветвей.

Уловка заключается в следующем: если я сначала оставлю его неинициализированным, а затем в длинном блоке if / else появится ошибка, поэтому переменная никогда не будет назначена, я могу увидеть эту ошибку в Valgrind :-), что, конечно же, требует частого запуска кода ( в идеале регулярные тесты) через Valgrind.

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

int i = 0;
struct myStruct m = {0};

Вы в основном добавляете 1 или 2 инструкции по сборке, если это так. Фактически, многие среды выполнения C сделают это за вас в сборке «Release», и вы ничего не измените.

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

Одна из причин отказа от инициализации связана с отладкой. Некоторые среды выполнения, например. MS CRT инициализирует память заранее заданными и задокументированными шаблонами, которые вы можете идентифицировать. Итак, когда вы просматриваете память, вы можете видеть, что память действительно не инициализирована и не использовалась и не сбрасывалась. Это может быть полезно при отладке. Но это во время отладки.

Это должно быть обязательно в основном. Причина этого не имеет ничего общего с представление, а скорее в опасности использования унифицированной переменной. Однако бывают случаи, когда это выглядит просто нелепо. Например, я видел:

struct stat s;
s.st_dev = -1;
s.st_ino = -1;
s.st_mode = S_IRWXU;
s.st_nlink = 0;
s.st_size = 0;
// etc...
s.st_st_ctime = -1;
if (stat(path, &s) != 0) {
   // handle error
   return;
}

Какого черта ???

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

memset (& s, ВЫБРАТЬ_ВАШ_ИНИЦИАЛИЗАТОР, sizeof (struct stat));

Adam Liss 12.11.2008 08:22

Если вы считаете, что инициализация избыточна, это так. Моя цель - написать код, максимально понятный для человека. Излишняя инициализация сбивает с толку будущего читателя.

Компиляторы C довольно хорошо умеют улавливать использование унифицированных переменных, поэтому опасность этого теперь минимальна.

Не забывайте, что, выполняя «фальшивую» инициализацию, вы обмениваете одну опасность - сбой при использовании мусора (что приводит к ошибке, которую очень легко найти и исправить) - другой - программа, выполняющая неправильные действия, основанные на фальшивом значении (что приводит к к ошибке, которую очень сложно найти). Выбор зависит от приложения. Для некоторых критически важно никогда не разбиться. Для большинства лучше отловить ошибку как можно скорее.

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

Mr. Shiny and New 安宇 26.09.2008 19:34

Собственно, указатель, инициализированный нулем (NULL), часто что-то означает. Вам лучше инициализировать что-то вроде 0xbaadf00d

buti-oxa 27.09.2008 02:58

@B Не уверен, что это настоящий компромисс, по моему опыту отслеживание ошибки, вызванной неопределенной переменной мусора, НАМНОГО сложнее, чем отслеживание ошибки, вызванной постоянно воспроизводимой неправильной инициализацией.

schmick 28.09.2008 17:51

Мне нравится использовать параметр "initauto" компилятора IBM xlc, чтобы помочь в этом. Для отладочных сборок у меня есть все автоматические переменные, инициализированные некоторым байтовым шаблоном, который легко обнаружить. Затем он отключается для сборок выпуска для повышения производительности.

Anthony Giorgio 16.09.2009 18:49

Шмик прав, ошибки, связанные с «использованием мусора», часто трудно отлаживать. Я говорил об обнаружении ошибки при тестировании, а не об обнаружении проблемы во время отладки. Слишком часто ошибки «неправильной инициализации» не обнаруживаются до поставки продукта.

buti-oxa 16.09.2009 22:15

Это относится только к C++, но между этими двумя методами есть определенное различие. Предположим, у вас есть класс MyStuff, и вы хотите инициализировать его другим классом. Вы можете сделать что-то вроде:

// Initialize MyStuff instance y
// ...
MyStuff x = y;
// ...

На самом деле это вызывает конструктор копирования x. Это то же самое, что:

MyStuff x(y);

Это отличается от этого кода:

MyStuff x; // This calls the MyStuff default constructor.
x = y; // This calls the MyStuff assignment operator.

Конечно, при построении копирования и при построении по умолчанию + присваивании вызывается совершенно другой код. Кроме того, одиночный вызов конструктора копирования, вероятно, будет более эффективным, чем построение с последующим присваиванием.

В качестве простого примера, можете ли вы определить, чем это будет инициализировано (C / C++)?

bool myVar;

У нас была проблема с продуктом, который иногда рисовал изображение на экране, а иногда нет, обычно в зависимости от того, на какой машине он был построен. Оказалось, что на моей машине он инициализировался как false, а на машине коллег инициализировался как true.

Если переменная является локальной в области видимости, то согласно K&R 4.9 она имеет «неопределенное (т.е. мусорное) начальное значение». Я подозреваю, что машина, на которой он был СОЗДАН, не имела ничего общего с вариациями в поведении; это было связано с состоянием машины, на которой он был ВЫПОЛНЕН. Извините, если мой вопрос был непонятен.

schmick 28.09.2008 16:44

Ах да. Спасибо, это все прояснилось. Всегда задавался вопросом, почему.

Mark Ingram 28.09.2008 20:44

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

Например:


void func(int n)
{
    int i = 0;

    ... // Many lines of code

    for (;i < n; i++)
        do_something(i);

Через некоторое время вы собираетесь добавить еще кое-что.


void func(int n)
{
    int i = 0;

    for (i = 0; i < 3; i++)
        do_something_else(i);

    ... // Many lines of code

    for (;i < n; i++)
        do_something(i);

Теперь ваш второй цикл не будет начинаться с 0, но с 3, в зависимости от того, что делает функция, может быть очень сложно обнаружить, что есть даже ошибка.

Это должно провалить проверку кода, потому что 'i' явно не инициализируется в цикле for (), что зависит на его начальном значении. Лучшие практики почти всегда могут быть нарушены худшими.

Adam Liss 12.11.2008 08:31

затем используйте цикл while или выполните здесь цикл while {} или функцию, которая принимает i в качестве аргумента, моя точка зрения заключается в том, что использование инициализации, которая не находится рядом с кодом, в котором вы ее используете, может привести к плохим результатам, если вы расширите функция позже.

quinmars 12.11.2008 12:12

Просто второстепенное наблюдение. Инициализации ЛЕГКО оптимизированы только для примитивных типов или когда они назначаются константными функциями.

а = foo ();

а = foo2 ();

Нельзя легко оптимизировать, потому что foo может иметь побочные эффекты.

Кроме того, выделение кучи раньше времени может привести к огромному снижению производительности. Возьмите код вроде

void foo(int x)

{

ClassA * instance = new ClassA ();

// ... делаем что-то не относящееся к экземпляру ... если (x> 5) {

delete instance;

return;

}

// .. делаем что-то, что использует экземпляр

}

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

изменить: я не могу использовать функцию листинга кода: P

Позвольте мне рассказать вам историю о продукте, над которым я работал в 1992 году, а затем, который мы будем называть Stackrobat. Мне назначили ошибку, которая приводила к сбою приложения на Mac, но не в Windows, о, и ошибка не воспроизводилась надежно. QA потребовалось больше недели, чтобы придумать рецепт, который сработал бы примерно в 1 из 10 раз.

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

В конце концов, я отследил это, написав собственный профилировщик кода для компилятора. Компилятор с радостью вставлял вызовы глобальным функциям prof_begin () и prof_end (), и вы могли реализовать их самостоятельно. Я написал профилировщик, который взял адрес возврата из стека, нашел инструкцию по созданию кадра стека, обнаружил блок в стеке, который представлял локальные переменные для функции, и покрыл их вкусным слоем дерьма, которое могло бы вызвать ошибку шины, если таковая имеется. разыменован элемент.

Это выявляло что-то вроде полдюжины ошибок указателей, использовавшихся перед инициализацией, включая ошибку, которую я искал.

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

Я потратил более двух недель, пытаясь найти эту ошибку.

Урок: инициализируйте своих локальных пользователей. Если кто-то кричит вам о производительности, покажите ему этот комментарий и скажите, что вы лучше потратите две недели на выполнение кода профилирования и устранение узких мест, чем на отслеживание подобных ошибок. Инструменты отладки и средства проверки кучи стали намного лучше с тех пор, как мне пришлось это сделать, но, честно говоря, они стали лучше, чтобы компенсировать ошибки, возникающие из-за таких плохих практик.

Если вы не работаете в крошечной системе (встроенной и т. д.), Инициализация локальных переменных должна быть почти бесплатной. Инструкции MOVE / LOAD выполняются очень и очень быстро. Сначала напишите код, чтобы он был надежным и поддерживаемым. Реорганизуйте его, чтобы он был производительным вторым.

Эту историю нужно преподавать на первом уроке любого класса для разработчиков программного обеспечения.

Davyd Geyl 23.05.2014 09:20

Да: всегда инициализирует ваши переменные, если у вас нет веской причины очень не делать этого. Если моему коду не требуется конкретное начальное значение, я часто инициализирую переменную значением, которое будет гарантия явной ошибкой, если следующий код сломан.

Другие вопросы по теме