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

Есть ли разница в производительности между 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 ++ic 6c6 < for (i = 0; i < 100; i++) --- > for (i = 0; i < 100; ++i) 

Мы скомпилируем их, а также получим сгенерированный ассемблер:

 $ gcc -c i++.c ++ic $ gcc -S i++.c ++ic 

И мы видим, что и сгенерированный объект, и файлы ассемблера совпадают.

 $ md5 i++.s ++is MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e MD5 (++is) = 90f620dda862cd0205cd5db1f2c8c06e $ md5 *.o MD5 (++io) = dd3ef1408d3a9e4287facccec53f7d22 MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22 

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

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

А также :

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

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

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

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

Однако, если i является сложным типом, вы можете найти измеримую разницу. Для i++ вы должны сделать копию своего classа, прежде чем увеличивать его. В зависимости от того, что связано с копией, оно действительно может быть медленнее, так как с ++it можно просто вернуть окончательное значение.

 Foo Foo::operator++() { Foo oldFoo = *this; // copy existing value - could be slow // yadda yadda, do increment return oldFoo; } 

Другое отличие состоит в том, что с ++i вас есть возможность вернуть ссылку вместо значения. Опять же, в зависимости от того, что участвует в создании копии вашего объекта, это может быть медленнее.

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

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

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

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

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

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

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

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

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

Длительный ответ:

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

В случае for(i=0; 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.
  • Хранить регистр A в i .

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

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

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

Таким образом, нет никакой разницы между префиксным и постфиксным ++ в C. Теперь то, что вы, как программист С, должно быть различным, - это люди, которые в некоторых случаях непоследовательно используют префикс и постфикс в других случаях, без каких-либо причин. Это говорит о том, что они не уверены в том, как работает 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 может выполнять операцию использования параллельно операции инкремента. Разница составляет не более одного цикла процессора, поэтому она действительно небрежна, но она есть. И наоборот, многие ожидали.

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

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

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

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

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

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

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

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

Представьте, что процессор с регистром A используется в качестве аккумулятора, и он является единственным регистром, используемым во многих инструкциях (на самом деле такие небольшие микроcontrollerы).

Теперь представьте следующую программу и их перевод в гипотетическую сборку:

Приращение приставки:

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

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


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

Я всегда предпочитаю pre-increment, однако …

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

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

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

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

 myArray[i++] = "hello"; 

против

 myArray[++i] = "hello"; 

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

Только мои два цента.

  • Разница между rdtscp, rdtsc: памятью и cpuid / rdtsc?
  • Точное измерение времени для тестирования производительности
  • Как мне зарегистрироваться на C # без использования сторонних библиотек?
  • Структура агрегации Mongodb: используется ли индекс использования группы?
  • Эффективность STL priority_queue
  • Делают ли c ++ шаблоны медленными?
  • Почему условный ход не уязвим для отказа от ветвления?
  • Эффективное умножение векторных матриц 4x4 на SSE: горизонтальное добавление и точечный продукт - в чем смысл?
  • Поведение коллекции мусора для String.intern ()
  • Какова цель регистра указателя кадров EBP?
  • Как вы проверяете время работы кода VBA?
  • Давайте будем гением компьютера.