Проблемы с обслуживанием, которые вызывают неинициализированные локальные переменные (особенно указатели), будут очевидны для любого, кто немного занимался обслуживанием или улучшением 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++, но, пожалуйста, четко укажите язык / среду (и воспроизводимые доказательства предпочтительнее предположений!)





Иногда вам нужна переменная в качестве заполнителя (например, при использовании функций ftime), поэтому нет смысла инициализировать их перед вызовом функции инициализации.
Однако, на мой взгляд, было бы неплохо отметить тот факт, что вы знаете о подводных камнях, что-то на пути
uninitialized time_t t;
time( &t );
Я не уверен, нужно ли «делать их обязательными», но лично я считаю, что всегда лучше инициализировать переменные. Если цель приложения - быть как можно более тесной, то для этой цели открыт C / C++. Однако я думаю, что многие из нас раз или два сгорели из-за того, что не инициализировали переменную и предполагали, что она содержит допустимое значение (например, указатель), хотя на самом деле это не так. Указатель с нулевым адресом намного легче проверить, чем наличие случайного мусора из последнего содержимого памяти в этом конкретном месте. Я думаю, что в большинстве случаев это уже не вопрос производительности, а вопрос ясности и безопасности.
Краткий ответ: объявите переменную как можно ближе к первому использованию и инициализируйте ее значением «ноль», если вам все еще нужно.
Длинный ответ: если вы объявляете переменную в начале функции и не используете ее позже, вам следует пересмотреть свое размещение переменной в максимально локальной области видимости. Затем вы можете сразу же присвоить ему необходимое значение.
Если вы должны объявить его неинициализированным, потому что он назначается в условном выражении или передается по ссылке и назначается, инициализация его значением, эквивалентным нулю, является хорошей идеей. Компилятор иногда может спасти вас, если вы компилируете под -Wall, так как он предупредит вас, если вы прочитаете переменную перед ее инициализацией. Однако он не предупреждает вас, если вы передадите его функции.
Если вы перестрахуетесь и установите для него нулевой эквивалент, вы не причините вреда, если функция, которой вы передадите его, перезапишет его. Если, однако, функция, которой вы ее передаете, использует значение, вы можете в значительной степени гарантировать, что не будет выполнено утверждение (если оно у вас есть) или, по крайней мере, произойдет отказ во втором случае, когда вы используете нулевой объект. Случайная инициализация может привести к разного рода плохим вещам, включая «работу».
Иногда вам нужно передать ссылку на функцию, которую нужно назначить. В этом случае сначала должна быть объявлена переменная.
общий случай переменной, объявленной перед циклом / условной структурой, поэтому вы можете установить значение в «цикле» и сохранить значение впоследствии
вы получите предупреждение с параметром -Wall, только если вы читаете переменную, в которую никогда не писали. Это отличается от кодирования в стиле «инициализировать на всякий случай».
Спасибо за комментарии; Я это починил. Надо было протестировать -Wall :(
@ShoeLace: Итак, если цикл вообще не повторяется, каково значение вашей переменной? Я бы сказал, что это именно та вещь, которую «всегда инициализировать» помогает предотвратить / выделить.
@H Ваши короткие и длинные не учитывают, что то, что вы предлагаете, невозможно в C, и даже если бы это было так, они не предполагают никаких проблем с производительностью при инициализации всего. Кроме того, я не вижу, как передача инициализированной переменной в функцию может быть чем угодно, но не ЛУЧШЕ, чем неопределенная?
Представление? Настоящее время? Возможно, когда процессоры работали на частоте 10 МГц, это имело смысл, но сегодня это вряд ли проблема. Всегда инициализируйте их.
Среди нас есть люди, которые до сих пор пишут для таких процессоров. Во встроенном мире происходит очень много инженерных разработок.
Есть также люди, которые пишут тяжелый код для обработки научных чисел, который работает в течение нескольких дней / недель. Даже улучшение на 1% является значительным. Но я согласен, что ваш компилятор должен быть настроен так, чтобы по умолчанию отмечать это как предупреждение.
Да уж, именно такие мысли делают наши современные компьютеры такими же медленными, как и старые. Новые программисты пишут раздутый код с менталитетом «теперь процессор работает быстро, мы можем себе это позволить».
Да, эм, я разрабатываю изрядное количество кода для коробки с частотой 40 МГц. Возможно, мне придется перенести его на коробку с частотой 8 МГц. Пожалуйста, не будьте небрежны в исполнении.
Нет, инициализация переменных не снижает производительность. Для этого вам нужно рассматривать библиотеки / языки, которые имеют копии памяти при любой возможности как функцию. Не оптимизируйте преждевременно, инициализация переменных не будет вашей проблемой.
Как вы показали в отношении перформанса, это не имеет значения. Компилятор (в оптимизированных сборках) обнаружит, записана ли локальная переменная без ее чтения, и удалит код, если он не имеет других побочных эффектов.
Тем не менее: если вы инициализируете материал простыми операторами, просто чтобы убедиться, что он инициализирован, это нормально ... Я лично не делаю этого по одной причине:
Это заставляет ребят, которые позже могут поддерживать ваш код, думать, что инициализация необходима. Этот маленький 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
Я ненавижу, когда люди цитируют только половину предложения и вырывают это из контекста, кстати ...
Не отменяет его точку зрения ... Я не думаю, что инициализация var - это критические 3%
Проблема здесь не в производительности, а в использовании. Вы не хотите использовать (читать) переменную, которая еще не была инициализирована.
@JJ Интересная статья Спасибо! Я думаю, что насмешки немного сильны ... может быть лучше с здравым смыслом :) Обратите внимание, что он признает свое невежество по этому поводу и говорит, не слушайте меня ...
@Herms: в вопросе говорится, что `` влияние на производительность приводится как оправдание ''. Это вопрос, к которому я обращаюсь
@B Спасибо, и я, конечно, согласен (хотя я не мог указать источник, мне в голову пришла та же цитата). Однако я хочу знать (по соображениям производительности), при каких обстоятельствах (если таковые имеются) НЕ стоит просто ЗАПРЕТИТЬ инициализировать локальную переменную.
@schmick Когда вы можете продемонстрировать, это означает значительную потерю производительности. Часто приходят на ум называемые тугие петли. Я БУДУ БЫ ОБЯЗАТЕЛЬНЫМ, но с исключением, если вы можете ДЕМОНСТРИРОВАТЬ
Иногда переменная используется для «сбора» результата более длинного блока вложенных 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));
Если вы считаете, что инициализация избыточна, это так. Моя цель - написать код, максимально понятный для человека. Излишняя инициализация сбивает с толку будущего читателя.
Компиляторы C довольно хорошо умеют улавливать использование унифицированных переменных, поэтому опасность этого теперь минимальна.
Не забывайте, что, выполняя «фальшивую» инициализацию, вы обмениваете одну опасность - сбой при использовании мусора (что приводит к ошибке, которую очень легко найти и исправить) - другой - программа, выполняющая неправильные действия, основанные на фальшивом значении (что приводит к к ошибке, которую очень сложно найти). Выбор зависит от приложения. Для некоторых критически важно никогда не разбиться. Для большинства лучше отловить ошибку как можно скорее.
Поддельные ценности может быть трудно обнаружить, если вы не можете сказать, что они поддельные. Например, указатель, инициализированный нулем, приводит к хорошей отказоустойчивой ситуации, если вы его разыменовываете. Но неинициализированный указатель может указывать на память, которую вы иногда можете использовать, что время от времени приводит к сбоям.
Собственно, указатель, инициализированный нулем (NULL), часто что-то означает. Вам лучше инициализировать что-то вроде 0xbaadf00d
@B Не уверен, что это настоящий компромисс, по моему опыту отслеживание ошибки, вызванной неопределенной переменной мусора, НАМНОГО сложнее, чем отслеживание ошибки, вызванной постоянно воспроизводимой неправильной инициализацией.
Мне нравится использовать параметр "initauto" компилятора IBM xlc, чтобы помочь в этом. Для отладочных сборок у меня есть все автоматические переменные, инициализированные некоторым байтовым шаблоном, который легко обнаружить. Затем он отключается для сборок выпуска для повышения производительности.
Шмик прав, ошибки, связанные с «использованием мусора», часто трудно отлаживать. Я говорил об обнаружении ошибки при тестировании, а не об обнаружении проблемы во время отладки. Слишком часто ошибки «неправильной инициализации» не обнаруживаются до поставки продукта.
Это относится только к 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 она имеет «неопределенное (т.е. мусорное) начальное значение». Я подозреваю, что машина, на которой он был СОЗДАН, не имела ничего общего с вариациями в поведении; это было связано с состоянием машины, на которой он был ВЫПОЛНЕН. Извините, если мой вопрос был непонятен.
Ах да. Спасибо, это все прояснилось. Всегда задавался вопросом, почему.
Я думаю, что в большинстве случаев это плохая идея инициализировать переменные значением по умолчанию, потому что это просто скрывает ошибки, которые легко найти с неинициализированными переменными. Если вы забудете получить и установить фактическое значение или случайно удалите код получения, вы, вероятно, никогда этого не заметите, потому что 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 (), что зависит на его начальном значении. Лучшие практики почти всегда могут быть нарушены худшими.
затем используйте цикл while или выполните здесь цикл while {} или функцию, которая принимает i в качестве аргумента, моя точка зрения заключается в том, что использование инициализации, которая не находится рядом с кодом, в котором вы ее используете, может привести к плохим результатам, если вы расширите функция позже.
Просто второстепенное наблюдение. Инициализации ЛЕГКО оптимизированы только для примитивных типов или когда они назначаются константными функциями.
а = 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 выполняются очень и очень быстро. Сначала напишите код, чтобы он был надежным и поддерживаемым. Реорганизуйте его, чтобы он был производительным вторым.
Эту историю нужно преподавать на первом уроке любого класса для разработчиков программного обеспечения.
Да: всегда инициализирует ваши переменные, если у вас нет веской причины очень не делать этого. Если моему коду не требуется конкретное начальное значение, я часто инициализирую переменную значением, которое будет гарантия явной ошибкой, если следующий код сломан.
Если бы вы сделали это обязательным, вы могли бы просто сделать это, что, на мой взгляд, имеет не меньший смысл: time_t t = {0};