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

Проблемы с обслуживанием, которые неинициализированные локальные жители вызывают (в частности, указатели), будут очевидны для всех, кто выполнил немного обслуживания или улучшения 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    $33, -4(%ebp)
>       movl    $4, 8(%esp)
>       movl    $66, 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/С++, но, пожалуйста, проясните язык/среду (и воспроизводимые доказательства намного предпочтительнее, чем спекуляции!)

Ответ 1

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

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

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

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

Ответ 2

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

Компиляторы

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

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

Ответ 3

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

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

Нет никаких сомнений в том, что грааль эффективности приводит к злоупотреблениям. Программисты тратят огромное количество времени на размышления о скорости некритических частей своих программ или беспокоятся о скорости их некритических частей, и эти попытки эффективности действительно оказывают сильное негативное влияние при отладке и обслуживании. Мы должны забыть о небольшой эффективности, скажем, около 97% времени: преждевременная оптимизация - корень всего зла.Однако мы не должны упускать наши возможности в этих критических 3%. Хороший программист не уклонится от самоуспокоения такими рассуждениями, он будет мудрым взглянуть на критический код; но только после того, как этот код был идентифицирован.

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

Чтобы вернуться к исходному вопросу: "Должен ли мы MANDATE инициализировать?"
Я бы назвал это так:

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

Ответ 4

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

Ответ 5

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

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

ВТФ???

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

Ответ 6

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

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

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

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

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

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

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

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

Ответ 7

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

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

uninitialized time_t t;
time( &t );

Ответ 8

Это относится только к С++, но между этими двумя методами существует определенное различие. Предположим, что у вас есть класс 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.

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

Ответ 9

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

Ответ 10

В 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 значением eqivilent false 0. Если я выполняю строковые операции, то ложная версия строки является пустой строкой, и это получает автоматически заменяется.

[[email protected] Code]$ ./undef.pl

1
2
Hello, SO!
5

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

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

: -)

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

Ответ 11

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

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

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

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

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

Ответ 12

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

Ответ 13

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

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

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

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

Например:

  float x = sqrt(0);

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

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

Ответ 14

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

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

Ответ 15

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

bool myVar;

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

Ответ 16

Я думаю, что в большинстве случаев плохая идея инициализировать переменные со значением по умолчанию, потому что она просто скрывает ошибки, которые легко найти с неинициализированными переменными. Если вы забыли получить и установить фактическое значение или случайно удалить код получения, вы, вероятно, никогда его не заметите, потому что 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, в зависимости от того, что функция может быть очень трудно найти, что есть даже ошибка.

Ответ 17

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

a = foo();

a = foo2();

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

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

void foo(int x)

{

ClassA * instance = new ClassA();

//... сделать что-то, не связанное с "экземпляром"... если (х > 5) {

delete instance;

return;

}

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

}

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

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