Оголошення змінних дороге?


75

Під час кодування на мові C я зіткнувся з наведеною нижче ситуацією.

Враховуючи, що ifоператор у наведеному вище коді може повернутися з функції, я можу оголосити змінні в двох місцях.

  1. Перед ifтвердженням.
  2. Після ifзаяви.

Як програміст, я хотів би зберегти декларацію змінної після ifStatement.

Місце декларації щось коштує? Або є якась інша причина віддавати перевагу одному шляху перед іншим?


5
Наскільки я розумію, розподіл простору стека - це просто збільшення покажчика стека. Скільки місця стеку потрібно функції, визначається під час компіляції, переглядаючи всі оголошення змінних. Ініціалізація змінної - це те, що відбувається під час виконання, тож це вплине на продуктивність.
Пітер Вітвоє

3
Подивіться на код асемблера, згенерований для обох варіантів - він повинен дати вам чітку відповідь. Просто переконайтеся, що ви використовуєте ті самі параметри оптимізації компілятора, що і для побудови випуску.
kestasx

67
Якщо ви дбаєте про ефективність, тоді у вас повинен бути спосіб виміряти ефективність вашої програми. Якщо у вас є можливість виміряти ефективність, тоді ви можете відповісти на власне запитання: спробуйте в обох напрямках, і ви скоро дізнаєтесь. Якщо у вас немає жодного методу для вимірювання ефективності, але ви дбаєте про ефективність, тоді ваше запитання має бути "як налаштувати інструменти для вимірювання своєї продуктивності?"
Eric Lippert

1
Чи можете ви це зробити на C ?
Salman A

2
@ g24l: Той факт, що на більшість питань можна відповісти однаково, вказує, чому такі питання погано підходять для цього веб-сайту. Прямо там "що робить цей код?" питання; ви написали код, запустите його і подивіться, що він робить, і тоді ви дізнаєтесь.
Ерік Ліпперт,

Відповіді:


97

У C99 та пізніших версіях (або із загальним відповідним розширенням до C89) ви можете змішувати оператори та декларації.

Так само, як і в попередніх версіях (лише тим більше, що компілятори стають розумнішими та агресивнішими), компілятор вирішує, як розподілити регістри та стек, або виконати будь-яку кількість інших оптимізацій, що відповідають правилу як-якщо-якщо.
Це означає, що з точки зору продуктивності жодної різниці не очікується.

У будь-якому випадку, це не була причиною дозволу такого:

Це було для обмеження сфери застосування, а отже, і для зменшення контексту, про який людина повинна пам’ятати, інтерпретуючи та перевіряючи ваш код.


1
дякую за вашу відповідь і всім. Якщо я правильно розумію ваше твердження, це не коштує жодної проблеми з продуктивністю, але виявляє ясність. Це так ?
Whoami

5
Якщо ви хочете бачити оптимізатор у дії, напишіть функцію з оголошеннями, які ініціалізуються як char *foo = "something", скомпілюйте свій код за допомогою прапорців оптимізатора та налагодження ( gcc -O3 -gнаприклад) і пройдіть через функцію в налагоджувачі. Точка кроку буде відскакувати і затримувати ініціалізацію змінної доти, доки вона їй не буде потрібна.
Schwern

5
+1за те, що тут зазначається важливість зменшення надходжень мозку ... багато людей про це забувають.

44

Робіть все, що має сенс, але поточний стиль кодування рекомендує розміщувати оголошення змінних якомога ближче до їх використання

Насправді оголошення змінних безкоштовні практично на кожному компіляторі після першого. Це пояснюється тим, що практично всі процесори управляють своїм стеком за допомогою вказівника стека (і, можливо, вказівника на кадр). Наприклад, розглянемо дві функції:

Якби я скомпілював їх на сучасному компіляторі (і сказав би, щоб він не був розумним та оптимізував мої невикористані локальні змінні), я б побачив це (приклад збірки x64 .. інші подібні):

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

Це пояснюється тим, що практично всі компілятори виділяють весь необхідний простір заздалегідь (за винятком вигадливих речей, таких як allocaякі обробляються окремо). Насправді на x64 обов’язково робити це таким ефективним способом.

(Редагувати: Як зазначав Форсс, компілятор може оптимізувати деякі локальні змінні в регістри. Більш технічно, я повинен аргументувати, що перший змінний, який "переливається" в стек, коштує 2 операційні коди, а решта безкоштовні)

З тих самих причин компілятори збиратимуть усі декларації локальних змінних та виділятимуть для них простір. C89 вимагає, щоб усі оголошення були попередніми, оскільки він був розроблений як однопрохідний компілятор. Щоб компілятор C89 знав, скільки місця потрібно виділити, йому потрібно було знати всі змінні перед тим, як видавати решту коду. У сучасних мовах, таких як C99 та C ++, компілятори, як очікується, будуть набагато розумнішими, ніж у 1972 році, тому це обмеження послаблюється для зручності розробників.

Сучасна практика кодування пропонує пропонувати змінні близько до їх використання

Це не має нічого спільного з компіляторами (яким, очевидно, так чи інакше було байдуже). Було виявлено, що більшість програмістів-людей краще читають код, якщо змінні розміщуються близько до місця їх використання. Це лише керівництво стилем, тому сміливо не погоджуйтеся з ним, але серед розробників існує надзвичайний консенсус, що це „правильний шлях”.

Тепер для кількох кутових випадків:

  • Якщо ви використовуєте C ++ з конструкторами, компілятор виділить простір спереду (оскільки це швидше зробити і не зашкодить). Однак змінна не буде побудована в цьому просторі, поки не буде правильно розміщено в потоці коду. У деяких випадках це означає, що наближення змінних до їх використання може бути навіть швидшим, ніж розміщення їх спереду ... контроль потоку може направити нас навколо оголошення змінної, і в цьому випадку конструктор навіть не потрібно викликати.
  • allocaобробляється на шарі над цим. Для тих, хто цікавиться, allocaреалізації, як правило, мають ефект переміщення покажчика стека вниз на якусь довільну величину. Функції, які використовуються alloca, потрібні для того, щоб так чи інакше відстежувати цей простір, а також переконайтеся, що вказівник стека переналаштовано вгору перед виходом.
  • Може трапитися так, що зазвичай вам потрібно 16 байт простору стека, але за однієї умови вам потрібно виділити локальний масив 50 кБ. Незалежно від того, куди ви вкладаєте свої змінні в код, практично всі компілятори виділяють 50 кБ + 16 Б місця в стеку кожного разу, коли функцію викликають. Це рідко має значення, але в нав'язливо рекурсивному коді це може переповнити стек. Вам або потрібно перенести код, що працює з масивом 50 кБ, у власну функцію, або використовувати alloca.
  • Деякі платформи (наприклад: Windows) потребують спеціального виклику функції в пролозі, якщо ви виділяєте більше місця на стосі. Це не повинно дуже сильно змінити аналіз (у реалізації це дуже швидка функція листа, яка просто тисне 1 слово на сторінку).

Будь ласка, будьте чіткими та скажіть C89, ISO C90, K&R C тощо. "Вимагає, щоб усі декларації були попередніми, оскільки вони були розроблені як 1-прохідний компілятор". Ви, очевидно, знаєте різницю, оскільки називаєте C99 як виняток, але C! = C89. Можливо, прямо зараз, C = C11.
Jeff Hammond

Зауважте також, що можна використовувати {} для змішування декларацій та коду в C89, за умови, що область дії працює.
Джефф Хаммонд,

@Jeff: Ах, я не знав, що ти можеш використовувати брекети таким чином. Думаю, це я отримую за те, що я людина, яка намагається відповісти на C ++, використовуючи термінологію C!
Корт Аммон

C ++ є надмножином C89, тому більшість часу у вас повинно бути добре. C99 вас зрозуміє, адже це не сувора підмножина C ++ 03.
Jeff Hammond

"Насправді оголошення змінних безкоштовні практично на кожному компіляторі після першого". Чи це правда, якщо ви розглядаєте використання більшої кількості змінних, ніж компілятор може оптимізувати в регістри?
Форсс

21

У C я вважаю, що всі оголошення змінних застосовуються так, ніби вони перебувають у верхній частині оголошення функції; якщо ви оголосите їх у блоці, я думаю, що це просто обсяг (не думаю, що це те саме в C ++). Компілятор виконує всі оптимізації змінних, а деякі можуть навіть ефективно зникати в машинному коді при більш високих оптимізаціях. Потім компілятор вирішить, скільки місця потрібно змінним, а потім, під час виконання, створить простір, відомий як стек, де живуть змінні.

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

Оголошення змінних як таке не є "дорогим"; якщо досить просто не використовуватись як змінну, компілятор, ймовірно, видалить її як змінну.

Заціни:

Da стек

Вікіпедія на стеках дзвінків , Деякі інші місця на стеку

Звичайно, все це залежить від реалізації та системи.


Як би це працювало з VLA? Або повністю усунувши змінну / це простір стека? Або згладжування слотів?
Дедулікатор

@Deduplicator Очевидно, це збільшує розмір кадру стека (згідно з цією презентацією). Це говорить alloca, але ці два пов’язані . allocaвиділяє простір зі стека. Нагадування: це визначено реалізацію.
Джеремі Роді

Просто думав підштовхнути вас до деяких конструкцій, ваше пояснення не дозволить зараз, тож ви можете вдосконалити це.
Дедулікатор

@Deduplicator Вибачте? Думаю, я не розумію, що ви намагаєтесь сказати.
Джеремі Роді

6
@Paul Незвично малювати стек "у зворотному порядку", якщо ви цього бажаєте (верх стопки, що знаходиться у верхній частині паперу, має очевидні переваги, якщо ви малюєте від руки). І в будь-якому випадку ви можете сказати HP, що вони робили це неправильно всі ці роки (PA-RISC традиційно стек росте вгору;) Чорт, якщо у вас немає pushта popоперацій (чи справді є якісь ISA RISC?) це просто умова - MULTICS мав зростаючі стеки.
Voo

12

Так, це може коштувати ясності. Якщо є випадок, коли функція не повинна взагалі нічого робити за якихось умов (як, наприклад, при пошуку глобальної помилки у вашому випадку), тоді розміщення чека вгорі, де ви його показуєте вище, зрозуміло простіше зрозуміти - щось важливе під час налагодження та / або документування.


11

Врешті-решт це залежить від компілятора, але зазвичай усі місцеві жителі виділяються на початку функції.

Однак вартість розподілу локальних змінних дуже мала, оскільки вони поміщаються в стек (або вносяться в реєстр після оптимізації).


"всі місцеві жителі виділяються на початку функції". ви маєте на увазі, якщо я також використовую лінивий підхід, чи його виділяють?
Whoami

Можливо, так. Як це робити, залежить від компілятора, але найпростіший спосіб - розподілити все на початку та звільнити все на кінець.
Мозковий штурм

2
Однак розподіл - це лише одна інструкція додавання, тому це неймовірно дешево. Зовсім не так, як використання malloc.
Мозковий штурм

Але, дивлячись на всі інші відповіді інженерів, здається, це не є продуктивністю чи якимись іншими витратами, окрім читабельності? :)
Whoami

7

Найкраща практика - адаптувати лінивий підхід, тобто заявляти про них лише тоді, коли вони вам справді потрібні;) (а не раніше). Це призводить до наступних переваг:

Код є більш читабельним, якщо ці змінні оголошені якомога ближче до місця використання.


4
Це неправильно (принаймні для мене). Я вважаю, що код набагато зручніший для читання (і для редагування - мені не потрібно проходити полювання за кодом, щоб змінити декларацію), якщо всі змінні оголошені в блоці у верхній частині функції, а не розкидані по коду. І як зазначали інші, компілятори досить розумні, щоб оптимізувати розподіл.
jamesqf

2
Для вас, можливо; але не для більшості кодової спільноти!
CinCout

3
Це неправильно, крапка. У прикладі використовуються змінні, які статично розміщені в стеку. Компілятор згенерує вказівку резервувати пам'ять у стеці для цих локальних змінних незалежно від того, де вони оголошені в рамках цієї функції.
Mark E. Haase

@binaryBaBa Ваш другий пункт "Ви в кінцевому підсумку не виділяєте пам'ять для змінних на більш ранньому етапі" не пов'язаний з читабельністю коду, і, як вже згадували інші, я не вважаю це правильним.
TJ

Так, це має сенс для мене, враховуючи вдосконалені компілятори зараз. Відповідно відредаговано.
CinCout

6

Зберігайте декларацію якомога ближче до місця, де вона використовується. В ідеалі всередині вкладених блоків. Тож у цьому випадку не було б сенсу оголошувати змінні над ifтвердженням.


5

Якщо у вас є це

тоді простір стека, зарезервований fooі someconditionможе бути очевидно повторно використаний для str1тощо, тому, оголосивши після if, ви можете заощадити простір стека. Залежно від можливостей оптимізації компілятора, економія місця у стеку може також мати місце, якщо ви згладжуєте функцію, видаляючи внутрішню пару фігурних дужок або якщо ви оголошуєте str1тощо перед if; однак для цього потрібно, щоб компілятор / оптимізатор зауважив, що сфери дії "насправді" не збігаються. Розміщуючи декларації після, ifви полегшуєте цю поведінку навіть без оптимізації - не кажучи вже про покращену читабельність коду.


5

Кожного разу, коли ви виділяєте локальні змінні в області дії C (наприклад, функції), вони не мають коду ініціалізації за замовчуванням (наприклад, конструктори C ++). І оскільки вони не розподіляються динамічно (це просто неініціалізовані вказівники), ніяких додаткових (і потенційно дорогих) функцій не потрібно викликати (наприклад,malloc для їх підготовки / розподілу не ).

Завдяки тому, як працює стек , виділення змінної стека просто означає зменшення покажчика стека (тобто збільшення розміру стека, оскільки на більшості архітектур він зростає вниз), щоб звільнити місце для нього. З точки зору центрального процесора, це означає виконання простої інструкції SUB:SUB rsp, 4 (у випадку, якщо ваша змінна має 4 байти - наприклад, звичайне 32-бітове ціле число).

Більше того, коли ви оголошуєте кілька змінних, ваш компілятор досить розумний, щоб насправді згрупувати їх в одну велику SUB rsp, XXінструкцію, де XXє загальний розмір локальних змінних області. Теоретично. На практиці трапляється щось трохи інше.

У таких ситуаціях я вважаю, що GCC Explorer є безцінним інструментом, коли справа доходить до виявлення (з надзвичайною легкістю) того, що відбувається "під капотом" компілятора.

Тож давайте подивимось, що відбувається, коли ви насправді пишете таку функцію: посилання GCC Explorer .

Код С

У результаті складання

Як виявляється, GCC ще розумніший за це. Він навіть не виконує інструкцію SUB для розподілу локальних змінних. Він просто (внутрішньо) припускає, що простір "зайнятий", але не додає жодних інструкцій щодо оновлення вказівника стека (наприклад SUB rsp, XX). Це означає, що покажчик стека не оновлюється, але, оскільки в цьому випадку після використання простору стека більше PUSHне виконуються інструкції (і відсутні rspвідносні пошуки), проблем немає.

Ось приклад, коли не оголошуються додаткові змінні: http://goo.gl/3TV4hE

Код С

У результаті складання

Якщо ви подивитесь на код перед передчасним поверненням ( jmp .L3який переходить до коду очищення та повернення), ніяких додаткових інструкцій для "підготовки" змінних стека не буде використано. Єдина відмінність полягає в тому, що параметри функції a і b, які зберігаються в регістрах ediand esi, завантажуються в стек за вищою адресою, ніж у першому прикладі ( [rbp-4]і[rbp - 8] ). Це пов’язано з тим, що додатковий простір не виділено для локальних змінних, як у першому прикладі. Отже, як бачите, єдиною "накладними витратами" для додавання цих локальних змінних є зміна терміна віднімання (тобто навіть не додавання додаткової операції віднімання).

Отже, у вашому випадку фактично відсутні витрати на просто оголошення змінних стека.


4

Я вважаю за краще зберігати умову "передчасного виходу" у верхній частині функції, крім того, щоб документувати, чому ми це робимо. Якщо ми ставимо його після купи оголошень змінних, хтось, хто не знайомий з кодом, може легко його пропустити, якщо не знає, що йому потрібно його шукати.

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


4

Якщо це насправді мало значення, єдиним способом уникнути розподілу змінних, швидше за все, буде:

Але на практиці, я думаю, ви не знайдете жодної переваги в роботі. Якщо що, незначні накладні витрати.

Звичайно, якщо ви кодували C ++ і деякі з цих локальних змінних мали нетривіальні конструктори, вам, ймовірно, потрібно було б розмістити їх після перевірки. Але навіть тоді я не думаю, що це допомогло б розділити функцію.


1

Якщо ви оголошуєте змінні після оператора if і негайно повертаєтеся з функції, компілятор не виділяє пам'ять у стек.


@ThomasPapamilhos: Не так. Відповідь насправді невизначена, але, на мій досвід, більшість компіляторів виділяють повний простір для завершення функції при вході, щоб уникнути повторних рухів покажчика стека, які майже нічого не отримують.
Persixty
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.