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

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

Ответы

Ответ 1

Резюме: Нет.

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

Ответ 2

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

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

А также:

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

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

Ответ 3

Лучший ответ заключается в том, что ++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++.

Ответ 4

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

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

Причина тому, если вы посмотрите на схему звонков операторов.

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

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

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

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

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

Ответ 5

Здесь дополнительное наблюдение, если вы беспокоитесь о микро оптимизации. Сокращение циклов может быть "возможно" более эффективным, чем приращение циклов (в зависимости от архитектуры набора команд, например 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.

Ответ 6

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

Нет никакой разницы между 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 или что у них неправильное знание языка. Это всегда плохой знак, он, в свою очередь, предполагает, что они делают другие сомнительные решения в своей программе, основанные на суевериях или "религиозных догмах".

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

Ответ 7

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

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

Ответ 8

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


К деталям.

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

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

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

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

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

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

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

Ответ 9

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

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

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

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

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

Ответ 10

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

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

Ответ 11

Я могу думать о ситуации, когда 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;).

Ответ 12

Я перечитал большинство ответов здесь и многие комментарии, и я не увидел ссылки на один экземпляр, который мог бы придумать, где i++ более эффективен, чем ++i (и, возможно, удивительно, что --i был более эффективным, чем i--). Это для компиляторов C для DEC PDP-11!

PDP-11 имел инструкции по сборке для предварительного уменьшения регистра и постинкрементного изменения, но не наоборот. Инструкции позволили использовать любой регистр общего назначения в качестве указателя стека. Поэтому, если вы использовали что-то вроде *(i++), это можно было бы скомпилировать в одну инструкцию по сборке, а *(++i) - нет.

Это, очевидно, очень эзотерический пример, но он предоставляет исключение, когда постинкремент более эффективен (или, я должен сказать, был, так как в наши дни нет большого спроса на код PDP-11 C).

Ответ 13

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

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

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

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

Ответ 14

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

myArray[i++] = "hello";

против

myArray[++i] = "hello";

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

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