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

c performance optimization post-increment pre-increment

95077 просмотра

14 ответа

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

Автор: Mark Harrison Источник Размещён: 04.10.2019 11:51

Ответы (14)


372 плюса

Решение

Резюме: Нет.

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
Автор: Mark Harrison Размещён: 24.08.2008 06:48

105 плюса

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

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

А также :

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

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

Автор: Sébastien RoccaSerra Размещён: 02.09.2008 11:48

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

Автор: Andrew Grant Размещён: 25.08.2008 04:49

16 плюса

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

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

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

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

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

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

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

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

Автор: JProgrammer Размещён: 25.08.2008 04:29

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.

Автор: tonylo Размещён: 02.09.2008 11:39

16 плюса

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

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

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

Автор: Lundin Размещён: 21.10.2014 09:10

11 плюса

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

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

Автор: Andy Lester Размещён: 20.09.2008 05:09

9 плюса

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


К деталям.

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

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

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

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

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

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

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

Автор: cmaster Размещён: 04.06.2014 06:34

7 плюса

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

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

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

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

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

Автор: Andreas Размещён: 09.02.2009 03:40

5 плюса

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

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

Автор: Kristopher Johnson Размещён: 24.08.2008 02:29

4 плюса

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

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

Автор: Shahbaz Размещён: 04.06.2014 05:57

4 плюса

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

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

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

Автор: daShier Размещён: 15.08.2019 10:45

2 плюса

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

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

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

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

Автор: Andrew Eidsness Размещён: 16.10.2008 11:28

0 плюса

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

myArray[i++] = "hello";

против

myArray[++i] = "hello";

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

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

Автор: Jason Z Размещён: 24.08.2008 02:22
32x32