Есть ли разница в производительности между i ++ и ++ i в C?

390

Существует ли разница в производительности между i++ и ++i если результирующее значение не используется?

  • 106
    Расслабьтесь только потому, что вы не понимаете, что обоснование не делает обоснование смешным или бессмысленным. Люди проголосовали за этот вопрос, потому что понимание ответа помогло им лучше понять кое-что о языке Си. Ответ также показывает интересный способ продемонстрировать, что два фрагмента кода семантически идентичны.
Теги:
optimization
performance
pre-increment
post-increment

13 ответов

348
Лучший ответ

Резюме: Нет.

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

Мы можем продемонстрировать это, посмотрев код этой функции, как с ++i и с i++.

$ cat i++.c
extern void g(int i);
void f()
{
 int i;

 for (i = 0; i < 100; i++)
  g(i);

}

Файлы одинаковы, кроме ++i и i++:

$ diff i++.c ++i.c
6c6
<  for (i = 0; i < 100; i++)
---
>  for (i = 0; i < 100; ++i)

Мы скомпилируем их, а также получим сгенерированный ассемблер:

$ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c

И мы видим, что и сгенерированный объект, и файлы ассемблера совпадают.

$ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e

$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22
  • 9
    Я знаю, что этот вопрос касается C, но мне было бы интересно узнать, могут ли браузеры выполнять эту оптимизацию для javascript.
  • 4
    Даже если бы компилятор не оптимизировал, вы вряд ли заметите разницу на любом современном процессоре.
Показать ещё 16 комментариев
87

От эффективности и намерения Эндрю Кенига:

Во-первых, далеко не очевидно, что ++i более эффективен, чем i++, по крайней мере, где целые переменные.

А также:

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

Итак, если результирующее значение не используется, я бы использовал ++i. Но не потому, что он более эффективен: потому что он правильно формулирует мое намерение.

  • 3
    Давайте не будем забывать, что другие унарные операторы также являются префиксами. Я думаю, что ++ i - это «семантический» способ использования унарного оператора, в то время как i ++ используется для удовлетворения конкретных потребностей (оценка перед добавлением).
  • 7
    Если полученное значение не используется, нет разницы в семантике: т. Е. Нет оснований для предпочтения той или иной конструкции.
Показать ещё 8 комментариев
44

Лучший ответ заключается в том, что ++i будет иногда быстрее, но не медленнее.

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

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

Foo Foo::operator++()
{
 Foo oldFoo = *this; // copy existing value - could be slow
 // yadda yadda, do increment
 return oldFoo;
}

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

Реальным примером того, где это может произойти, будет использование итераторов. Копирование итератора вряд ли будет бутылочной горловиной в вашем приложении, но все же хорошей практикой является привычка использовать ++i вместо i++ где результат не i++.

  • 31
    Вопрос явно говорит о C, без ссылки на C ++.
  • 4
    Этот (по общему признанию) старый вопрос касался C, а не C ++, но я полагаю, что стоит также упомянуть, что в C ++, даже если класс реализует операторы пост- и предфикса, они даже не обязательно связаны между собой. Например, bar ++ может увеличивать один элемент данных, в то время как ++ bar может увеличивать другой элемент данных, и в этом случае у вас не будет возможности использовать любой из них, так как семантика различна.
Показать ещё 2 комментария
16

Здесь дополнительное наблюдение, если вы беспокоитесь о микро оптимизации. Сокращение циклов может быть "возможно" более эффективным, чем приращение циклов (в зависимости от архитектуры набора команд, например ARM), учитывая:

for (i = 0; i < 100; i++)

На каждом цикле вы будете иметь по одной инструкции для:

  1. Добавление 1 в i.
  2. Сравните ли i меньше 100.
  3. Условная ветвь, если i меньше 100.

В то время как декрементирующий цикл:

for (i = 100; i != 0; i--)

Цикл будет иметь инструкцию для каждого из:

  1. Уменьшить i, установив флаг состояния ЦП.
  2. Условная ветвь зависит от состояния регистра процессора (Z==0).

Конечно, это работает только при уменьшении до нуля!

Помните о руководстве разработчика системы ARM.

  • 0
    Неплохо. Но разве это не создает меньше попаданий в кеш?
  • 0
    В книгах по оптимизации кода есть старый странный трюк, сочетающий преимущество ветки с нулевым тестом и увеличенного адреса. Вот пример на языке высокого уровня (вероятно, бесполезный, так как многие компиляторы достаточно умны, чтобы заменить его менее эффективным, но более распространенным циклическим кодом): int a [N]; для (i = -N; i; ++ i) a [N + i] + = 123;
Показать ещё 5 комментариев
14

Получение листа от Скотта Мейерса, более эффективное c++ Пункт 6: Различать префиксные и постфиксные формы операций приращения и декремента.

Префиксная версия всегда предпочтительнее постфикса относительно объектов, особенно в отношении итераторов.

Причина этого, если вы посмотрите на шаблон вызова операторов.

// Prefix
Integer& Integer::operator++()
{
 *this += 1;
 return *this;
}

// Postfix
const Integer Integer::operator++(int)
{
 Integer oldValue = *this;
 ++(*this);
 return oldValue;
}

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

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

Но поскольку вы указываете на int, нет никакой разницы из-за оптимизации компилятора, которая может иметь место.

  • 4
    Я думаю, что его вопрос был направлен на C, но для C ++ вы абсолютно правы, и, кроме того, люди C должны принять это, так как они могут использовать его и для C ++. Я слишком часто вижу программистов на C, использующих синтаксис postfix ;-)
  • 0
    -1 Хотя это хороший совет для программистов на C ++, он ни в малейшей степени не отвечает на вопрос, помеченный тегом C. В C абсолютно не имеет значения, используете ли вы префикс или постфикс.
Показать ещё 2 комментария
11

Короткий ответ:

Нет никакой разницы между i++ и ++i с точки зрения скорости. Хороший компилятор не должен генерировать другой код в двух случаях.

Длительный ответ:

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

В случае for(i=0; i<n; i++), i++ является единственным в своем собственном выражении: перед i++ есть точка последовательности, а после него - одна. Таким образом, только генерируемый машинный код - "увеличить i на 1 ", и он четко определен, как это упорядочивается по отношению к остальной части программы. Поэтому, если вы измените его на префикс ++, это ни в коем случае не имеет значения, вы все равно просто получите машинный код "увеличить i на 1 ".

Различия между ++i и i++ значение только в выражениях, таких как array[i++] = x; против array[++i] = x; , Некоторые могут возразить и сказать, что постфикс будет медленнее в таких операциях, потому что регистр, в котором i проживаю, должен быть перезагружен позже. Но тогда обратите внимание, что компилятор может свободно заказывать ваши инструкции любым способом, если это не "нарушает поведение абстрактной машины", как называет его стандарт C.

Поэтому, хотя вы можете предположить, что array[i++] = x; преобразуется в машинный код как:

  • Хранить значение i в регистре A.
  • Хранить адрес массива в регистре B.
  • Добавьте A и B, сохраните результаты в A.
  • На этом новом адресе, представленном A, сохраните значение x.
  • Хранить значение i в регистре A//неэффективно, потому что дополнительная инструкция здесь, мы уже сделали это один раз.
  • Инкрементный регистр A.
  • Хранить регистр A в i.

компилятор может также повысить эффективность кода, например:

  • Хранить значение i в регистре A.
  • Хранить адрес массива в регистре B.
  • Добавьте A и B, сохраните результаты в B.
  • Инкрементный регистр A.
  • Хранить регистр A в i.
  • ...//остальная часть кода.

Просто потому, что вы, как программист С, обучены думать, что постфикс ++ происходит в конце, машинный код не нужно заказывать таким образом.

Таким образом, нет никакой разницы между префиксным и постфиксным ++ в C. Теперь то, что вы, как программист С, должно быть различным, - это люди, которые в некоторых случаях непоследовательно используют префикс и постфикс в других случаях, без каких-либо причин. Это говорит о том, что они не уверены в том, как работает C или что у них неправильное знание языка. Это всегда плохой знак, он, в свою очередь, предполагает, что они делают другие сомнительные решения в своей программе, основанные на суевериях или "религиозных догмах".

"Префикс ++ всегда быстрее" - это действительно одна из таких ложных догм, которая распространена среди потенциальных программистов на С.

  • 2
    Никто не сказал: «Префикс ++ всегда быстрее». Это неправильно процитировано. Они сказали, что «Postfix ++ никогда не быстрее».
  • 1
    @Pacerier Я не цитирую какого-то конкретного человека, я просто распространяю неправильные убеждения.
Показать ещё 4 комментария
10

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

Используйте то, что имеет смысл для человека, читающего код.

  • 3
    Я считаю, что неправильно предпочесть нечеткие улучшения читабельности фактическому повышению эффективности и общей ясности намерений.
  • 4
    Мой термин «время чтения программистом» примерно аналогичен «ясности намерений». «Фактическое повышение эффективности» часто неизмеримо, достаточно близко к нулю, чтобы назвать их нулем. В случае OP, если код не был профилирован, чтобы обнаружить, что ++ i является узким местом, вопрос о том, какой из них быстрее, является пустой тратой времени и мыслительными единицами программиста.
Показать ещё 3 комментария
9

Прежде всего: разница между i++ и ++i нечетна в C.


К деталям.

1. Известная проблема C++: ++i быстрее

В C++, ++i более эффективен, если i - это какой-то объект с перегруженным оператором приращения.

Зачем?
В ++i объект сначала увеличивается, а затем может передаваться как константная ссылка на любую другую функцию. Это невозможно, если выражение foo(i++) потому что теперь приращение нужно выполнить до foo(), но старое значение нужно передать foo(). Следовательно, компилятор вынужден сделать копию i до того, как он выполнит оператор инкремента в оригинале. Дополнительные вызовы конструктора/деструктора являются плохой частью.

Как отмечено выше, это не относится к фундаментальным типам.

2. Малоизвестный факт: i++ может быть быстрее

Если не нужно вызывать конструктор/деструктор, что всегда имеет место в C, ++i и i++ должны быть одинаково быстрыми, верно? Нет. Они практически одинаково быстры, но могут быть небольшие различия, которые большинство других ответчиков ошибались.

Как i++ может быть быстрее?
Точка - это зависимости данных. Если значение нужно загрузить из памяти, необходимо выполнить две последующие операции с ним, увеличить его и использовать. С помощью ++i необходимо выполнить ++i до того, как значение будет использоваться. С помощью i++ использование не зависит от приращения, а ЦП может выполнять операцию использования параллельно операции инкремента. Разница составляет не более одного цикла процессора, поэтому она действительно небрежна, но она есть. И наоборот, многие ожидали.

  • 0
    О вашей точке 2: Если ++i или i++ используется в другом выражении, переключение между ними изменяет семантику выражения, поэтому любой возможный выигрыш / потеря производительности не обсуждается. Если они автономны, т. Е. Результат операции не используется сразу, то любой приличный компилятор скомпилирует его так же, например инструкцию сборки INC .
  • 0
    @ Shahbaz Это совершенно верно, но не в этом суть. 1) Несмотря на то, что семантика различна, i++ и ++i можно использовать взаимозаменяемо практически в любой возможной ситуации, регулируя константы цикла на единицу, поэтому они почти эквивалентны тем, что они делают для программиста. 2) Несмотря на то, что оба компилируют одну и ту же инструкцию, их выполнение для ЦП различно. В случае i++ ЦП может вычислять приращение параллельно с какой-либо другой инструкцией, которая использует то же значение (ЦП действительно делают это!), В то время как с ++i ЦП должен планировать другую инструкцию после приращения.
Показать ещё 3 комментария
7

@Mark Несмотря на то, что компилятору разрешено оптимизировать временную копию переменной (на основе стека) и gcc (в последних версиях), это не значит, что все компиляторы всегда будут делать это.

Я просто тестировал его с помощью компиляторов, которые мы используем в нашем текущем проекте, и 3 из 4 не оптимизируют его.

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

Если у вас нет действительно глупой реализации одного из операторов в вашем коде:

Alwas предпочитает ++i по сравнению с i++.

  • 0
    Просто любопытно ... почему вы используете 4 разных компилятора Си в одном проекте? Или в одной команде или в одной компании?
  • 3
    При создании игр для консолей каждая платформа использует собственный компилятор / набор инструментов. В идеальном мире мы могли бы использовать gcc / clang / llvm для всех целей, но в этом мире нам приходится мириться с Microsoft, Intel, Metroworks, Sony и т. Д.
4

Я могу думать о ситуации, когда postfix медленнее, чем приращение приставки:

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

Теперь представьте следующую программу и их перевод в гипотетическую сборку:

Приращение приставки:

a = ++b + c;

; increment b
LD A, [&b]
INC A
ST A, [&b]

; add with c
ADD A, [&c]

; store in a
ST A, [&a]

Приращение притчи:

a = b++ + c;

; load b
LD A, [&b]

; add with c
ADD A, [&c]

; store in a
ST A, [&a]

; increment b
LD A, [&b]
INC A
ST A, [&b]

Обратите внимание, как значение b было принудительно перезагружено. С приращением префикса компилятор может просто увеличить значение и продолжать использовать его, возможно, не перезагружать его, так как желаемое значение уже находится в регистре после приращения. Тем не менее, с приращением postfix, компилятор должен иметь дело с двумя значениями: один - старый и один - с добавленным значением, которое, как показано выше, приводит к еще одному доступу к памяти.

Конечно, если значение приращения не используется, например, одно i++; , компилятор может (и делает) просто сгенерировать инструкцию приращения независимо от использования постфикса или префикса.


В качестве примечания я хотел бы упомянуть, что выражение, в котором есть b++ не может быть просто преобразовано в одно с ++b без каких-либо дополнительных усилий (например, добавив a- - 1). Поэтому сравнение двух, если они являются частью некоторого выражения, действительно недействительно. Часто, когда вы используете b++ внутри выражения, вы не можете использовать ++b, поэтому, даже если ++b потенциально более эффективен, это было бы просто неправильно. Исключение, конечно, если выражение попросит его (например, a = b++ + 1; который можно изменить на a = ++b;).

4

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

Однако в C++ при использовании других типов, которые предоставляют свои собственные операторы ++, префиксная версия, вероятно, будет быстрее, чем версия postfix. Итак, если вам не нужна семантика postfix, лучше использовать префиксный оператор.

2

Я всегда предпочитаю pre-increment, однако...

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

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

Кроме того, предоставление optmizer менее вероятно, означает, что компилятор работает быстрее.

  • 0
    Ваша публикация специфична для C ++, а вопрос о C. В любом случае, ваш ответ неверен: для пользовательских операторов постинкрементного компилятора, как правило, не удастся создать столь эффективный код.
  • 0
    Он заявляет, «если функция становится встроенной», и это делает его рассуждение правильным.
Показать ещё 1 комментарий
0

Мой C немного ржавый, поэтому я заранее извиняюсь. Скорость, я могу понять результаты. Но я смущен тем, как оба файла выходили в один и тот же MD5-хэш. Может быть, цикл for работает одинаково, но не будут ли следующие две строки кода генерировать другую сборку?

myArray[i++] = "hello";

против

myArray[++i] = "hello";

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

Только мои два цента.

  • 1
    @Jason Z Оптимизация компилятора происходит до того, как сборка будет завершена, будет видно, что переменная i больше нигде не используется в той же строке, поэтому удержание ее значения будет пустой тратой, и, вероятно, она перевернет ее на i ++. Но это только предположение. Я не могу дождаться, пока один из моих лекторов попытается сказать, что это быстрее, и я стану тем парнем, который исправляет теорию с помощью практических доказательств. Я уже почти чувствую вражду ^ _ ^
  • 3
    «Мой C немного ржавый, поэтому я прошу прощения заранее». Ничего плохого в вашем C, но вы не прочитали оригинальный вопрос полностью: «Есть ли разница в производительности между i ++ и ++ i, если полученное значение не используется? » Обратите внимание на курсив. В вашем примере, «результат» из я ++ / ++ я используется, но в идиоматических для цикла, «результат» оператора до / postincrement не используется , так что компилятор может делать то , что ему нравится.
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню