Чи є накладні витрати на оголошення змінної в циклі? (С ++)


158

Мені просто цікаво, чи не було б втрати швидкості або ефективності, якщо ви зробите щось подібне:

int i = 0;
while(i < 100)
{
    int var = 4;
    i++;
}

який заявляє int varсто разів. Мені здається, що це було б, але я не впевнений. чи було б практичніше / швидше зробити це замість цього:

int i = 0;
int var;
while(i < 100)
{
    var = 4;
    i++;
}

або вони однакові, швидкісні та ефективні?


7
Щоб бути зрозумілим, наведений вище код не "оголошує" var сто разів.
jason

1
@Rabarberski: Питання, на яке посилається, не є точним дублікатом, оскільки не вказує мови. Це питання є специфічним для C ++ . Але відповідно до відповідей, розміщених на ваше запитання, на яке посилається, відповідь залежить від мови та, можливо, компілятора.
DavidRR

2
@jason Якщо перший фрагмент коду не оголошує змінну 'var' сто разів, чи можете ви пояснити, що відбувається? Це просто оголошує змінну один раз і ініціалізує її 100 разів? Я міг би подумати, що код оголошує та ініціалізує змінну 100 разів, оскільки все в циклі виконується 100 разів. Дякую.
randomUser47534

Відповіді:


194

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


50
Я хотів би, щоб ті хлопці, які викладають в коледжі, принаймні знали це основне. Одного разу він сміявся з того, що я оголошував змінну всередині циклу, і мені було цікаво, що не так, поки він не назвав продуктивність причиною цього не робити, і я був схожий на "WTF !?".
mmx

18
Ви впевнені, що вам слід негайно поговорити про простір стека. Така змінна також може бути в реєстрі.
Тото

3
@toto Така змінна також може бути ніде - varзмінна ініціалізується, але ніколи не використовується, тому розумний оптимізатор може просто повністю її видалити (за винятком другого фрагмента, якщо змінна була використана десь після циклу).
CiaPan,

@Mehrdad Afshari змінна в циклі отримує свій конструктор, що викликається один раз за ітерацію. РЕДАГУВАТИ - Я бачу, що ви згадали про це нижче, але я думаю, що це також заслуговує на згадування у прийнятій відповіді.
hoodaticus

106

Для примітивних типів і типів POD це не має значення. Компілятор виділить простір стека для змінної на початку функції та звільнить його, коли функція повернеться в обох випадках.

Для типів класів, що не є POD, які мають нетривіальні конструктори, це ЗМІНИТЬ різницю - у такому випадку виведення змінної поза цикл буде викликати конструктор і деструктор лише один раз, а оператор присвоєння кожну ітерацію, тоді як поміщати її всередину циклу буде викликати конструктор і деструктор для кожної ітерації циклу. Залежно від того, що роблять конструктор, деструктор та оператор присвоєння класу, це може бути або не бути бажаним.


42
Правильна ідея неправильна причина. Змінна поза циклу. Побудований один раз, знищений один раз, але оператор присвоєння застосовує кожну ітерацію Змінна всередині циклу. Constructe / Desatructor застосовував кожну ітерацію, крім нульових операцій присвоєння.
Мартін Йорк,

8
Це найкраща відповідь, але ці коментарі бентежать. Існує велика різниця між викликом конструктора та оператором присвоєння.
Ендрю Грант,

1
Це є істинним , якщо тіло циклу виконує завдання в будь-якому випадку, а не тільки для ініціалізації. І якщо є просто незалежна від тіла / постійна ініціалізація, оптимізатор може її підняти.
peterchen

7
@Andrew Grant: Чому. Оператор присвоєння зазвичай визначається як конструкція копіювання в tmp, за якою слідує обмін (щоб бути безпечним для винятків), а потім знищення tmp. Таким чином, оператор присвоєння не настільки відрізняється від цикла побудови / руйнування вище. Див. Stackoverflow.com/questions/255612/… для прикладу типового оператора присвоєння.
Мартін Йорк,

1
Якщо побудова / руйнування дорогі, їх загальна вартість є розумною верхньою межею вартості оператора =. Але завдання справді могло бути дешевшим. Крім того, коли ми розширюємо це обговорення від ints до типів C ++, можна узагальнити 'var = 4' як якусь іншу операцію, крім 'призначити змінну зі значення того самого типу'.
greggo

69

Вони обидва однакові, і ось як ви можете це дізнатись, подивившись, що робить компілятор (навіть без оптимізації, встановленої на високу):

Подивіться, що робить компілятор (gcc 4.0) на ваших простих прикладах:

1.c:

main(){ int var; while(int i < 100) { var = 4; } }

gcc -S 1.c

1.s:

_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $0, -16(%ebp)
    jmp L2
L3:
    movl    $4, -12(%ebp)
L2:
    cmpl    $99, -16(%ebp)
    jle L3
    leave
    ret

2.c

main() { while(int i < 100) { int var = 4; } }

gcc -S 2.c

2.s:

_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    $0, -16(%ebp)
        jmp     L2
L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3
        leave
        ret

З них видно дві речі: по-перше, код однаковий в обох.

По-друге, сховище для var виділяється поза циклом:

         subl    $24, %esp

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

L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3

Що приблизно настільки ефективно, наскільки ви можете бути, не видаляючи цикл повністю.


2
"Що є настільки ж ефективним, наскільки ви можете бути, не видаляючи цикл повністю" Не зовсім. Часткове розгортання циклу (якщо це робити 4 рази за прохід) різко пришвидшить його. Є, мабуть, багато інших способів оптимізації ... хоча більшість сучасних компіляторів, мабуть, зрозуміли б, що немає жодного сенсу в циклі. Якщо пізніше було використано "i", воно просто встановило б "i" = 100.
Даррон,

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

Як і оригінальний пост!
Алекс Браун,

2
Мені подобаються відповіді, які підтверджують теорію доказами! Приємно бачити, як дамп ASM підтримує теорію рівності кодів. +1
Хаві Монтеро,

1
Я фактично отримав результати, створивши машинний код для кожної версії. Не потрібно запускати його.
Alex Brown,

14

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

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


4
Я сумніваюся, що це вплине на оптимізацію - якщо компілятор виконує будь-який аналіз потоку даних, він може зрозуміти, що він не модифікується поза циклом, тому він повинен створити однаковий оптимізований код в обох випадках.
Адам Розенфілд,

3
Це не зрозуміє, якщо у вас є дві різні петлі, що використовують одне і те ж ім'я змінної темп.
Джошуа

11

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


3
Я насправді не вважаю це оптимізацією. Оскільки вони є локальними змінними, простір стека просто виділяється на початку функції. Справжнє "створення" не зашкоджує продуктивності (якщо не викликається конструктор, що зовсім інша історія).
mmx

Ви маєте рацію, "оптимізація" - неправильне слово, але я втрачаю краще.
Ендрю Заєць

проблема полягає в тому, що такий оптимізатор буде використовувати аналіз діапазону в реальному часі, і обидві змінні досить мертві.
MSalters

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

9

Для вбудованого типу, швидше за все, не буде різниці між 2 стилями (можливо, аж до згенерованого коду).

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

Класу RAII може знадобитися така поведінка. Наприклад, для належного управління доступом до файлу може знадобитися створити та знищити клас, який керує доступом до файлу на кожній ітерації циклу.

Припустимо, у вас є LockMgrклас, який отримує критичний розділ при його побудові та випускає його при знищенні:

while (i< 100) {
    LockMgr lock( myCriticalSection); // acquires a critical section at start of
                                      //    each loop iteration

    // do stuff...

}   // critical section is released at end of each loop iteration

зовсім відрізняється від:

LockMgr lock( myCriticalSection);
while (i< 100) {

    // do stuff...

}

6

Обидві петлі мають однакову ефективність. Вони обидва займуть нескінченну кількість часу :) Може бути непоганою ідеєю збільшити i всередині петель.


Ах, так, я забув звернутися до космічної ефективності - це нормально - 2 інти для обох. Мені просто здається дивним, що програмістам не вистачає лісу для дерева - всі ці пропозиції щодо коду, який не закінчується.
Ларрі Ватанабе,

Це нормально, якщо вони не припинять свою діяльність. Жоден з них не викликається. :-)
Носредна

2

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


Різниця походить, мабуть, від різниці в масштабі. Чим менший обсяг, тим більша ймовірність того, що компілятор зможе усунути серіалізацію змінної. У невеликому діапазоні циклу змінна, швидше за все, була внесена до реєстру і не збережена на кадрі стека. Якщо ви викликаєте функцію в циклі або перенаправляєте вказівник, компілятор насправді не знає, куди він вказує, він розллє змінну циклу, якщо вона знаходиться в області дії функції (вказівник може містити &i).
Патрік Шлютер

Будь ласка, опублікуйте налаштування та результати.
jxramos

2
#include <stdio.h>
int main()
{
    for(int i = 0; i < 10; i++)
    {
        int test;
        if(i == 0)
            test = 100;
        printf("%d\n", test);
    }
}

Код вище завжди друкує 100 10 разів, що означає, що локальна змінна всередині циклу виділяється лише один раз за кожен виклик функції.


0

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

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


"Єдиний спосіб бути впевненим - це встановити час". -1 неправда. Вибачте, але інший пост довів це неправильно, порівнявши створену машинну мову та виявивши її по суті ідентичною. Я взагалі не маю жодної проблеми з вашою відповіддю, але чи не помиляється, що таке -1?
Білл К

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

-1

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


-2

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


-6

це неправда, є накладні витрати, однак накладні витрати, які можуть нехтувати.

Незважаючи на те, що, ймовірно, вони опиняться там же в стеці, він все одно призначає це. Він призначить місце пам’яті в стеку для цього int, а потім звільнить його в кінці}. Не в сенсі вільного купи в сенсі він перемістить sp (покажчик стека) на 1. А у вашому випадку, якщо врахувати, що він має лише одну локальну змінну, він просто зрівняє fp (покажчик на кадр) і sp

Короткою відповіддю буде: НЕ ДОГЛЯДАЙТЕ, ЩОБ ТАК РАБОТИ ПРИБЛИЖНО ТЕЖ.

Але спробуйте прочитати більше про те, як організовано стек. У моїй студентській школі були досить хороші лекції з цього питання. Якщо ви хочете прочитати більше, перевірте тут http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Assembler1/lecture.html


Знову -1 неправда. Прочитайте пост, який розглядав збори.
Білл К

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