Вопрос

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

Это было полезно?

Решение

Управляющее резюме:Нет.

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

Взяв листок у Скотта Мейерса, Более эффективный С++ Пункт 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.

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

Никогда не бывает разницы между 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 // неэффективно, потому что здесь дополнительная инструкция, мы уже делали это один раз.
  • Регистр приращения А.
  • Сохраните регистр A в i.

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

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

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

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

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

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

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

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


К деталям.

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

myArray[i++] = "hello";

против

myArray[++i] = "hello";

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top