Избегайте постфиксного приращения оператора


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++делать разные вещи в определенных контекстах, не думайте, что они одинаковы.
Orbling

2
Это про С или С ++? Это два совершенно разных языка! :-) В C ++ идиоматический цикл for есть for (type i = 0; i != 42; ++i). Не только может operator++быть перегружен, но так может operator!=и operator<. Приращение префикса не дороже, чем постфикс, неравенство не дороже, чем меньше. Какие из них мы должны использовать?
Бо Перссон

7
Разве это не должно называться ++ C?
Арманд

21
@Stephen: C ++ означает взять C, добавить к нему, а затем использовать старый .
Суперкат

Ответы:


58

Факты:

  1. i ++ и ++ одинаково легко читаются. Он вам не нравится, потому что вы к нему не привыкли, но, по сути, вы не можете его неправильно истолковать, так что читать и писать больше не нужно.

  2. По крайней мере, в некоторых случаях постфиксный оператор будет менее эффективным.

  3. Тем не менее, в 99,99% случаев это не имеет значения, потому что (а) он будет работать с простым или примитивным типом в любом случае, и это проблема, только если он копирует большой объект; (б) он не будет работать критическая часть кода (с), вы не знаете, оптимизирует ли это компилятор или нет, это может сделать.

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

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

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

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

РЕДАКТИРОВАТЬ 1: Скотт Мейерс, в "Более эффективном C ++", которому я обычно доверяю в этом вопросе, говорит, что в общем следует избегать использования оператора postfix для пользовательских типов (поскольку единственная разумная реализация функции приращения postfix - это создание копии объекта, вызовите функцию приращения префикса для выполнения приращения и верните копию, но операции копирования могут быть дорогими).

Итак, мы не знаем, существуют ли какие-либо общие правила относительно (а) того, верно ли это сегодня, (б) применимо ли оно (в меньшей степени) к внутренним типам (в) следует ли использовать «++» для что-нибудь большее, чем легкий класс итераторов. Но по всем причинам, которые я описал выше, не имеет значения, делай то, что я сказал раньше.

РЕДАКТИРОВАТЬ 2: Это относится к общей практике. Если вы думаете, что это имеет значение в каком-то конкретном случае, то вам следует профилировать его и посмотреть. Профилирование легко и дешево и работает. Исходя из первых принципов, то, что нужно оптимизировать, сложно, дорого и не работает.


Ваше объявление прямо на деньги. В выражениях, где операторы infix + и post-increment ++ были перегружены, такие как aClassInst = someOtherClassInst +щеAnotherClassInst ++, синтаксический анализатор будет генерировать код для выполнения аддитивной операции, прежде чем генерировать код для выполнения операции после инкремента, что устраняет необходимость создать временную копию. Убийца производительности здесь не пост-приращение. Это использование перегруженного инфиксного оператора. Инфиксные операторы создают новые экземпляры.
bit-twiddler

2
Я очень подозреваю, что причина, по которой люди «привыкли», i++а не кроется ++iв названии определенного популярного языка программирования, упомянутого в этом вопросе / ответе ...
Shadow

61

Всегда кодируйте сначала программиста, а компьютера - второго.

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


7
SUPERB заявление !!!
Дейв

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

1
Для индекса цикла это было бы более понятно - но если бы вы перебирали массив, увеличивая указатель и используя префикс, это означало, что начинать с недопустимого адреса перед запуском было бы плохо, независимо от повышения производительности
Мартин Беккет,

5
@ Мэтью: Это просто неправда, что постинкремент подразумевает сохранение копии старого значения. Нельзя быть уверенным в том, как компилятор обрабатывает промежуточные значения, пока не будет просмотрен его вывод. Если вы потратите время на просмотр моего аннотированного списка сгенерированных GCC ассемблеров, вы увидите, что GCC генерирует одинаковый машинный код для обоих циклов. Эта бессмыслица о предпочтении предварительного увеличения перед постинкрементным, потому что он более эффективен, немного больше, чем гипотеза.
bit-twiddler

2
@Mathhieu: код, который я разместил, был создан с отключенной оптимизацией. В спецификации C ++ не указывается, что компилятор должен создавать временный экземпляр значения при использовании постинкрементации. Он просто устанавливает приоритет операторов до и после приращения.
bit-twiddler

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: Вероятно, без изменений, но ожидаете ли вы, что bit-twiddler вернется в ближайшее время?
Дедупликатор

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

@Deduplicator: Спасибо, также за указание на то, что этот ответ не распространяется на пользовательские типы. Я не смотрел на его страницу пользователя, прежде чем спрашивать.
Серв-ин

12

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

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

Теперь, когда это вне нашего пути, давайте сделаем наш выбор разумно :

  • ++i: приращение префикса , увеличение текущего значения и получение результата
  • i++: увеличение постфикса , копирование значения, увеличение текущего значения, получение копии

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

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

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

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


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

Прочитав статью, я не нахожу ее очень убедительной по трем причинам. Во-первых, компилятор должен иметь возможность оптимизировать создание объекта, который никогда не используется. Во- i++вторых , эта концепция идиоматична для числовых циклов for , поэтому случаи, на которые я могу обратить внимание, ограничены. В-третьих, они дают чисто теоретический аргумент, без цифр, подтверждающих это.

Основываясь на причине № 1, я полагаю, что когда вы действительно сделаете выбор времени, они будут рядом друг с другом.


-1

Прежде всего это не влияет на читабельность ИМО. Это не то, что вы привыкли видеть, но пройдет совсем немного времени, прежде чем вы привыкнете к этому.

Во-вторых, если вы не используете тонну постфиксных операторов в своем коде, вы вряд ли увидите большую разницу. Основным аргументом для того, чтобы не использовать их, когда это возможно, является то, что копия оригинального значения переменной var должна храниться до конца аргументов, в которых еще можно использовать исходный аргумент var. Это 32 или 64 бит в зависимости от архитектуры. Это соответствует 4 или 8 байтов, или 0,00390625, или 0,0078125 МБ. Шансы очень высоки, что если вы не используете тонну из них, которые нужно сохранять в течение очень длительного периода времени, при сегодняшних компьютерных ресурсах и скорости, вы даже не заметите разницу при переходе с постфикса на префикс.

РЕДАКТИРОВАТЬ: Забудьте эту оставшуюся часть, так как мой вывод оказался ложным (за исключением части ++ i и i ++, которые не всегда делают одно и то же ... это все еще верно).

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

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 ... Спасибо, я не был уверен, когда это увеличение произошло, потому что у меня никогда не было причин его проверять. Это было слишком давно, так как я делал компиляторы в колледже! лол
Кеннет

Неправильно неправильно неправильно
руохола

-1

Я думаю, что семантически, ++iимеет больше смысла, чем i++, поэтому я бы придерживался первого, за исключением того, что обычно это не делается (как в Java, где вы должны использовать, i++потому что он широко используется).


-2

Это не только производительность.

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

И использование разных приращений для примитивных типов и сложных типов ... это действительно нечитаемо.


-2

Если вам это действительно не нужно, я бы придерживался ++ i. В большинстве случаев это то, что каждый намерен. Я не очень часто нуждаюсь в i ++, и вам всегда нужно дважды подумать, читая такую ​​конструкцию. С ++ i это легко: вы добавляете 1, используете его, а затем я все тот же.

Итак, я полностью согласен с @martin beckett: сделай это проще для себя, это уже достаточно сложно.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.