Уникайте оператора збільшення Postfix


25

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

Але це не впливає на читабельність коду? На мою думку:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

Виглядає краще, ніж:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

Але це, мабуть, просто за звичкою. Правда, я не бачив багато користі ++i.

Чи в цьому випадку продуктивність погана для читання? Або я просто сліпий і ++iчитабельніший ніж i++?


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

15
++iі i++робити різні речі в певних контекстах, не вважайте, що вони однакові.
Орблінг

2
Це про C чи C ++? Це дві дуже різні мови! :-) У C ++ ідіоматичний фор-цикл є for (type i = 0; i != 42; ++i). Не тільки можна operator++перевантажувати, але так можна operator!=і operator<. Приріст префікса не дорожчий за постфікс, не рівний - не дорожчий, ніж менший. Які з них нам використовувати?
Бо Персон

7
Чи не слід його називати ++ C?
Арман

21
@Stephen: C ++ означає взяти C, додати його та використовувати старий .
supercat

Відповіді:


58

Факти:

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

  2. Принаймні в деяких випадках оператор postfix буде менш ефективним.

  3. Однак у 99,99% випадків це не має значення, оскільки (а) воно все одно буде діяти на простий або примітивний тип, і це проблема лише в тому випадку, якщо він буде копіювати великий об'єкт (б), він не буде в виконанні критична частина коду (с), яку ви не знаєте, оптимізує її компілятор чи ні, це може зробити.

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

Ви повинні використовувати здоровий глузд і не робити мікрооптимізацію, поки не потрібно, але й бути радикально неефективною заради цього. Як правило, це означає: по-перше, виключіть будь-яку побудову коду, яка є неприйнятно неефективною навіть у некритичному за часом коді (як правило, щось, що представляє фундаментальну концептуальну помилку, як, наприклад, передача об'єктів 500 МБ за значенням без будь-якої причини); по-друге, з кожного іншого способу написання коду вибирайте найясніший.

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

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

EDIT 1: Скотт Майєрс у "Ефективнішій C ++", якому я взагалі довіряю цю річ, говорить, що вам слід взагалі уникати використання оператора postfix для визначених користувачем типів (тому що єдиною розумною реалізацією функції збільшення постфікса є створення скопіюйте об'єкт, зателефонуйте на функцію збільшення префікса, щоб виконати приріст, і поверніть копію, але операції з копіювання можуть бути дорогими).

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

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


Ваша публікація прямо на гроші. У виразах, де оператор infix + та пост-інкремент ++ були перевантажені, такі як aClassInst = someOtherClassInst + yetAbodyClassInst ++, аналізатор генерує код для виконання операції добавки до генерації коду для виконання операції після збільшення. створити тимчасову копію. Вбивця продуктивності тут не збільшується. Це використання перевантаженого оператора інфіксації. Оператори Infix створюють нові екземпляри.
біт-твідлер

2
Я дуже підозрюю, що причина людей звикає, i++а не ++iчерез назву певної популярної мови програмування, на яку йдеться в цьому питанні / відповіді ...
Shadow

61

Завжди кодуйте програміста першим, а комп'ютером - другим.

Якщо є різниця у продуктивності, після того, як компілятор кинув експертне око на ваш код, І ви можете виміряти його І це має значення - тоді ви можете змінити його.


7
Заява СУПЕРБ !!!
Дейв

8
@Martin: саме тому я б використовував приріст приставки. Семантика Postfix передбачає збереження старого значення навколо, і якщо в цьому немає необхідності, використовувати його неправильно.
Матьє М.

1
Для індексу циклу, що було б зрозуміліше - але якщо ви повторювали масив, збільшуючи вказівник і використовуючи префікс, означало починати з незаконної адреси один до початку, це було б погано незалежно від підвищення продуктивності
Martin Beckett

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

2
@Mathhieu: Код, який я розмістив, створювався з оптимізацією, вимкнено. У специфікації C ++ не зазначено, що компілятор повинен створювати тимчасовий екземпляр значення, коли використовується пост-інкрементація. Він лише констатує перевагу операторів до та після збільшення.
біт-трейдлер

13

GCC виробляє однаковий машинний код для обох циклів.

C Кодекс

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

Кодекс Асамблеї (з моїми коментарями)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols

Як щодо ввімкнення оптимізації?
серв-ін

2
@user: Напевно, змін не буде, але ви насправді очікуєте, що біт-твілінг повернеться незабаром?
Дедуплікатор

2
Будьте уважні: Хоча в C немає визначених користувачем типів з перевантаженими операторами, у C ++ є, і узагальнення від базових типів до визначених користувачем типів просто недійсне .
Дедуплікатор

@Deduplicator: Дякую, що ви також вказали, що ця відповідь не узагальнює типи, визначені користувачем. Я не переглянув його сторінку користувача, перш ніж запитати.
серв-інк

12

Не турбуйтеся про продуктивність, скажімо, 97% часу. Передчасна оптимізація - це корінь зла.

- Дональд Кнут

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

  • ++i: приріст префікса , приріст поточного значення і дає результат
  • i++: приріст постфіксу , копіювання значення, збільшення поточного значення, отримання копії

Якщо копія старого значення не потрібна, використання приросту постфікса - це спосіб, який можна зробити.

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

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

Отже, не має значення, виглядає це правильно на вас, чи ні. Обійми KISS . Через кілька місяців ви будете уникати своїх старих практик.


4

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

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

Отже, підказка: Ви програмуєте на C або C ++, а питання стосуються того чи іншого, а не обох.


2

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

У випадку ++ i, ми повинні

Fetch i from memory 
Increment i
Store i back to memory
Use i

У випадку i ++ ми повинні

Fetch i from memory
Use i
Increment i
Store i back to memory

Оператори ++ і - відстежують своє походження до набору інструкцій PDP-11. PDP-11 може виконувати автоматичний пост-інкремент в регістрі. Він також може виконати автоматичну попередню постанову на ефективну адресу, що міститься в реєстрі. У будь-якому випадку компілятор міг би скористатися цими операціями на рівні машини лише тоді, коли змінна, про яку йдеться, була змінною "регістр".


2

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

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

Особливо, зважаючи на причину №1, я здогадуюсь, що коли ви фактично виконаєте терміни, вони будуть прямо поруч.


-1

Перш за все, це не впливає на читаність IMO. Це не те, що ви звикли бачити, але це було б лише короткий час, перш ніж ви звикли до нього.

По-друге, якщо ви не використовуєте в своєму коді тону операторів постфіксу, ви, ймовірно, не побачите великої різниці. Основним аргументом щодо їх використання, коли це можливо, є те, що копія значення оригіналу var повинна зберігатися до кінця аргументів, де ще можна було використовувати оригінальний var. Це або 32 біти, або 64 біт, залежно від архітектури. Це дорівнює 4 або 8 байтам або 0,00390625 або 0,0078125 Мб. Швидше за все, що, якщо ви не використовуєте багато тонн, які потрібно зберегти протягом дуже тривалого періоду часу, то, використовуючи сучасні комп'ютерні ресурси та швидкість, ви навіть не помітите різниці, переходячи від постфікса до префікса.

EDIT: Забудьте про цю частину, що залишилася, оскільки мій висновок виявився помилковим (за винятком частин ++ i та i ++, які не завжди роблять те саме ... це все-таки так).

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

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}

4
Операція приросту відбувається в кінці циклу for, тому вони мали б точно такий же вихід. Це не залежить від компілятора / інтерпретатора.
jsternberg

@jsternberg ... Дякую, я не був впевнений, коли приріст стався, оскільки я ніколи не мав причин коли-небудь перевірити його. Це було занадто довго, оскільки я робив компілятори в коледжі! lol
Кеннет

Неправильно неправильно неправильно.
ruohola

-1

Думаю, семантично ++iмає сенс більше i++, тому я б дотримувався першого, за винятком випадків , коли цього не робити (як, наприклад, у Java, де ви повинні використовувати, i++оскільки це широко використовується).


-2

Справа не лише у виконанні.

Іноді ви хочете взагалі уникати копіювання, оскільки це не має сенсу. А оскільки використання приросту префікса не залежить від цього, просто простіше дотримуватися форми префікса.

І використання різних приростів для примітивних типів і складних типів ... це справді нечитабельно.


-2

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

Отож, я погоджуюся з @martin beckett: полегшіть себе, це вже досить важко.

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