Является ли объявление переменных дорогостоящим?

При кодировании в C я столкнулся с ситуацией ниже.

int function ()
{
  if (!somecondition) return false;

  internalStructure  *str1;
  internalStructure *str2;
  char *dataPointer;
  float xyz;

  /* do something here with the above local variables */    
}

Учитывая, что оператор if в приведенном выше коде может вернуться из функции, я могу объявить переменные в двух местах.

  • Перед оператором if.
  • После инструкции if.

Как программист, я бы подумал сохранить объявление переменной после if Statement.

Записывает ли объявление что-то? Или есть еще одна причина предпочесть один путь друг другу?

Ответ 1

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

Как и в более ранних версиях (только в том случае, когда компиляторы стали более умными и агрессивными), компилятор решает, как распределять регистры и стек, или делать любое количество других оптимизаций, соответствующих правилу as-if.
Это означает, что по производительности, нет никаких ожиданий каких-либо различий.

Во всяком случае, это была не та причина, по которой это было разрешено:

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

Ответ 2

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

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

int foo() {
    int x;
    return 5; // aren't we a silly little function now
}

int bar() {
    int x;
    int y;
    return 5; // still wasting our time...
}

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

foo:
push ebp
mov  ebp, esp
sub  esp, 8    ; 1. this is the first line which is different between the two
mov  eax, 5    ; this is how we return the value
add  esp, 8    ; 2. this is the second line which is different between the two
ret

bar:
push ebp
mov  ebp, esp
sub  esp, 16    ; 1. this is the first line which is different between the two
mov  eax, 5     ; this is how we return the value
add  esp, 16    ; 2. this is the second line which is different between the two
ret

Примечание: обе функции имеют одинаковое количество кодов операций!

Это связано с тем, что практически все компиляторы будут выделять все пространство, в котором они нуждаются, перед началом (запрещающие причудливые вещи, такие как alloca, которые обрабатываются отдельно). На самом деле, на x64, это необходимо, чтобы они делали это эффективным образом.

(Edit: Как указывал Forss, компилятор может оптимизировать некоторые локальные переменные в регистры. Более технически я должен утверждать, что первая переменная, которая "перетекает" в стек, стоит 2 кода opcodes, а остальные - бесплатно)

По тем же причинам компиляторы будут собирать все объявления локальных переменных и выделять пространство для них прямо вверх. C89 требует, чтобы все объявления были первыми, потому что он был разработан как компилятор с 1 пропускным сигналом.. Чтобы компилятор C89 знал, сколько места для выделения, ему необходимо было знать все переменные перед выпуском остаток кода. В современных языках, таких как C99 и С++, компиляторы должны быть намного умнее, чем они были в 1972 году, поэтому это ограничение расслаблено для удобства разработчиков.

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

Это не имеет ничего общего с компиляторами (что, очевидно, не могло быть так или иначе). Было обнаружено, что большинство программистов-программистов лучше читают код, если переменные помещаются близко к тому, где они используются. Это просто руководство по стилю, поэтому не стесняйтесь не соглашаться с ним, но среди разработчиков есть замечательный консенсус, что это "правильный путь".

Теперь для нескольких угловых случаев:

  • Если вы используете С++ с конструкторами, компилятор будет выделять пространство вперед (так как это быстрее сделать так, и это не повредит). Однако переменная не будет построена в этом пространстве до правильного местоположения в потоке кода. В некоторых случаях это означает, что переменные, близкие к их использованию, могут быть даже быстрее, чем устанавливать их вверх... управление потоком может направлять нас вокруг объявления переменной, и в этом случае конструктор даже не нужно вызывать.
  • alloca обрабатывается над слоем выше этого. Для тех, кто любопытен, реализации alloca имеют тенденцию влиять на перемещение указателя стека на некоторую произвольную сумму. Функции, использующие alloca, необходимы для того, чтобы отслеживать это пространство так или иначе, и убедитесь, что указатель стека снова отрегулирован вверх, прежде чем уйти.
  • Может быть случай, когда вам обычно требуется 16-байтовое пространство стека, но при одном условии вам нужно выделить локальный массив размером 50 КБ. Независимо от того, где вы помещаете свои переменные в код, практически все компиляторы будут выделять 50kB + 16B пространства стека каждый раз при вызове функции. Это редко имеет значение, но в навязчиво рекурсивном коде это может переполнить стек. Вам либо нужно переместить код, работающий с массивом 50kB, в свою собственную функцию, либо использовать alloca.
  • Для некоторых платформ (например: Windows) требуется специальный вызов функции в прологе, если вы выделяете больше места на стеке. Это не должно сильно изменять анализ (в реализации это очень быстрая функция листа, которая просто выдает 1 слово на страницу).

Ответ 3

В C я считаю, что все объявления переменных применяются так, как если бы они находились в верхней части объявления функции; если вы объявляете их в блоке, я думаю, что это всего лишь предметная область (я не думаю, что это то же самое в С++). Компилятор выполнит все оптимизации по переменным, а некоторые могут даже эффективно исчезнуть в машинный код при более высоких настройках. Затем компилятор определит, сколько пространства потребуется переменным, а затем, во время выполнения, создайте пространство, известное как стек, в котором живут переменные.

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

Объявление переменных не является "дорогостоящим", как таковое; если он достаточно прост, чтобы не использоваться в качестве переменной, компилятор, вероятно, удалит его как переменную.

Проверьте это:

Da stack

Википедия на столах вызовов, Некоторое место в стеке

Конечно, все это зависит от реализации и зависит от системы.

Ответ 4

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

Ответ 5

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

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

Ответ 6

Держите объявление как можно ближе к тому, где оно используется. Идеально внутри вложенных блоков. Поэтому в этом случае нет смысла объявлять переменные выше инструкции if.

Ответ 7

Лучшей практикой является адаптация ленивого подхода, т.е. объявления их только тогда, когда они вам действительно нужны;) (а не раньше). Это приводит к следующему преимуществу:

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

Ответ 8

Если у вас есть этот

int function ()
{
   {
       sometype foo;
       bool somecondition;
       /* do something with foo and compute somecondition */
       if (!somecondition) return false;
   }
   internalStructure  *str1;
   internalStructure *str2;
   char *dataPointer;
   float xyz;

   /* do something here with the above local variables */    
}

тогда пространство стека, зарезервированное для foo и somecondition, может быть явно использовано повторно для str1 и т.д., поэтому, объявив после if, вы можете сохранить пространство стека. В зависимости от возможностей оптимизации компилятора сохранение пространства стека может также иметь место, если вы сглаживаете fucntion, удаляя внутреннюю пару фигурных скобок или если вы объявляете str1 и т.д. До if; однако для этого требуется, чтобы компилятор/оптимизатор заметил, что области не перекрываются. Положив объявления после if, вы облегчаете это поведение даже без оптимизации - не говоря уже о улучшенной читаемости кода.

Ответ 9

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

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

Ответ 10

Если это действительно имело значение, единственный способ избежать выделения переменных, вероятно, будет следующим:

int function_unchecked();

int function ()
{
  if (!someGlobalValue) return false;
  return function_unchecked();
}

int function_unchecked() {
  internalStructure  *str1;
  internalStructure *str2;
  char *dataPointer;
  float xyz;

  /* do something here with the above local variables */    
}

Но на практике я думаю, что вы не найдете выгоды от производительности. Если что-то незначительное накладное.

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

Ответ 11

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

В связи с тем, что работает стек, выделение переменной стека просто означает декремент указателя стека ( т.е. увеличение размера стека, потому что на большинстве архитектур оно растет вниз), чтобы освободить место для него. С точки зрения ЦП это означает выполнение простой инструкции SUB: SUB rsp, 4 (в случае, если ваша переменная имеет размер 4 байта - например, 32-битное целое число).

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

В таких ситуациях я нахожу GCC explorer бесценным инструментом, когда дело доходит до выяснения (с огромной легкостью), что происходит "под капотом" компилятора.

Итак, давайте посмотрим, что произойдет, когда вы на самом деле напишите такую ​​функцию: ссылка проводника GCC.

C-код

int function(int a, int b) {
  int x, y, z, t;

  if(a == 2) { return 15; }

  x = 1;
  y = 2;
  z = 3;
  t = 4;

  return x + y + z + t + a + b;
}

Результирующая сборка

function(int, int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-20], edi
    mov DWORD PTR [rbp-24], esi
    cmp DWORD PTR [rbp-20], 2
    jne .L2
    mov eax, 15
    jmp .L3
.L2:
    -- snip --
.L3:
    pop rbp
    ret

Как оказалось, GCC даже умнее этого. Он вообще не выполняет команду SUB для распределения локальных переменных. Он просто (внутренне) предполагает, что пространство "занято", но не добавляет никаких инструкций для обновления указателя стека (например, SUB rsp, XX). Это означает, что указатель стека не обновляется, но, поскольку в этом случае не выполняются инструкции PUSH (и нет rsp -relative lookups) после использования пространства стека, нет проблем.

Здесь пример, где не объявлены дополнительные переменные: http://goo.gl/3TV4hE

C-код

int function(int a, int b) {
  if(a == 2) { return 15; }
  return a + b;
}

Результирующая сборка

function(int, int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov DWORD PTR [rbp-8], esi
    cmp DWORD PTR [rbp-4], 2
    jne .L2
    mov eax, 15
    jmp .L3
.L2:
    mov edx, DWORD PTR [rbp-4]
    mov eax, DWORD PTR [rbp-8]
    add eax, edx
.L3:
    pop rbp
    ret

Если вы посмотрите на код перед преждевременным возвратом (jmp .L3, который переходит на код очистки и возврата), для "подготовки" переменных стека не требуются дополнительные инструкции. Единственное отличие состоит в том, что параметры функции a и b, которые хранятся в регистрах edi и esi, загружаются в стек с более высоким адресом, чем в первом примере ([rbp-4] и [rbp - 8]). Это связано с тем, что дополнительное пространство не было "выделено" для локальных переменных, как в первом примере. Итак, как вы можете видеть, единственными "накладными расходами" для добавления этих локальных переменных являются изменение вычитаемого термина (т.е. Даже не добавление дополнительной операции вычитания).

Таким образом, в вашем случае практически нет затрат на простое объявление переменных стека.

Ответ 12

Если вы объявляете переменные после инструкции if и возвращались из функции сразу, компилятор не использует память обязательств в стеке.