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


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

13 395

13 ответов:

резюме: нет.

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

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

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

и :

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

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

лучший ответ:++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++ где результат не влияет.

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

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

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

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

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

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

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

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

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

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

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

ответ:

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

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

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

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

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

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

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

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

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

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

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

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

прежде всего: разница между i++ и ++i является неглубоким в C.


в подробности.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

Я всегда предпочитаю предварительное приращение, однако ...

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

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

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

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

myArray[i++] = "hello";

vs

myArray[++i] = "hello";

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

просто мои два цента.