Есть ли разница в производительности между i ++ и ++ i в C? - PullRequest
425 голосов
/ 24 августа 2008

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

Ответы [ 13 ]

359 голосов
/ 24 августа 2008

Резюме: №

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
98 голосов
/ 02 сентября 2008

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

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

А:

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

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

43 голосов
/ 25 августа 2008

Лучшим ответом будет то, что ++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++, когда на результат не влияет.

16 голосов
/ 02 сентября 2008

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

16 голосов
/ 25 августа 2008

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

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

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

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

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

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

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

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

14 голосов
/ 21 октября 2014

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

Нет никакой разницы между 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 в i.

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

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

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

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

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

11 голосов
/ 20 сентября 2008

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

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

9 голосов
/ 04 июня 2014

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


к деталям.

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

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

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

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

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

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

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

7 голосов
/ 09 февраля 2009

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

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

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

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

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

5 голосов
/ 24 августа 2008

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

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

...