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

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

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

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

Там никогда нет никакой разницы между 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 в регистре А.
  • Сохранить адрес массива в регистре B.
  • Добавьте A и B, сохраните результаты в A.
  • По этому новому адресу, представленному буквой A, сохраните значение x.
  • Хранить стоимость i в регистре A // неэффективно, потому что здесь дополнительная инструкция, мы уже делали это один раз.
  • Инкрементный регистр А.
  • Хранить регистр А в i,

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

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

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

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

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

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


К деталям.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

myArray[i++] = "hello";

против

myArray[++i] = "hello";

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

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

Другие вопросы по тегам