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

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

Каков ответ на C++?

Я изменил теги, так как эти два тега - самый простой способ найти вопросы такого рода. Я также перебрал другие, у которых не было связных тегов, и дал им связующие теги.

George Stocker 10.09.2009 19:25

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

new123456 26.06.2011 05:53

Статья: Разумно ли использовать префиксный оператор приращения ++ it вместо постфиксного оператора it ++ для итераторов? - viva64.com/en/b/0093

user965097 16.04.2015 17:35
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
371
3
86 577
18
Перейти к ответу Данный вопрос помечен как решенный

Ответы 18

Ответ принят как подходящий

[Краткое содержание: используйте ++i, если у вас нет особой причины использовать i++.]

Для C++ ответ немного сложнее.

Если i является простым типом (не экземпляром класса C++), затем ответ на C («Нет разницы в производительности») сохраняется, поскольку компилятор генерирует код.

Однако, если i является экземпляром класса C++, то i++ и ++i вызывают одну из функций operator++. Вот стандартная пара этих функций:

Foo& Foo::operator++()   // called for ++i
{
    this->data += 1;
    return *this;
}

Foo Foo::operator++(int ignored_dummy_value)   // called for i++
{
    Foo tmp(*this);   // variable "tmp" cannot be optimized away by the compiler
    ++(*this);
    return tmp;
}

Поскольку компилятор не генерирует код, а просто вызывает функцию operator++, невозможно оптимизировать переменную tmp и связанный с ней конструктор копирования. Если конструктор копирования стоит дорого, это может существенно повлиять на производительность.

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

Blaisorblade 15.01.2009 03:13

i ++ на одну инструкцию процессора медленнее

Joe Phillips 15.01.2009 04:08

Не может ли компилятор полностью избежать этого, если оператор ++ встроен?

Eduard - Gabriel Munteanu 18.02.2009 18:59

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

Zan Lynx 09.09.2009 05:21

Отметьте: «this-> data» находится в одном примере, а «this-> mydata» - в другом ... вы можете это исправить?

John Zwinck 28.11.2009 21:50

Поведение C++ не так уж отличается от C. Если вы думаете о назначении памяти или регистров, то C++ показывает, что компилятор C должен делать за кулисами. Также многие компиляторы C++ будут использовать operator ++ (), если не определен operator ++ (int) (и выдают предупреждение).

kriss 31.03.2010 15:26

@kriss: разница между C и C++ заключается в том, что в C у вас есть гарантия, что оператор будет встроен, и в этот момент приличный оптимизатор сможет удалить разницу; вместо этого в C++ вы не можете предполагать встраивание - не всегда.

Blaisorblade 05.03.2012 14:45

Я бы +1, ЕСЛИ в ответе упоминалось что-то о классах, которые содержат указатели (автоматические, интеллектуальные или примитивные) на динамически выделяемую (кучу) память, где конструктор копирования обязательно выполняет глубокие копии. В таких случаях аргумента нет, ++ i, возможно, на порядок эффективнее, чем i ++. Ключевым моментом для них является выработка привычки использовать преинкремент всякий раз, когда семантика постинкремента на самом деле не требуется вашему алгоритму, и тогда вы будете иметь привычку писать код, который по своей природе обеспечивает большую эффективность, независимо от того, как ну, ваш компилятор может оптимизировать.

phonetagger 17.07.2012 22:57

Для тех, кто может быть сбит с толку фиктивным аргументом в operator++(int), он просто используется, чтобы различать две формы. Какая-то неряшливая, если вы спросите меня.

corazza 14.08.2014 19:02

Кто-нибудь может прокомментировать эту тему, если это std::atomic<int> или аналогичный?

Pedro Lamarão 23.06.2016 00:35

Я не понимаю, как в C вы можете гарантировать встраивание, а в C++ - нет?

rubenvb 18.07.2017 11:39

@rubenvb: В C нет такой вещи, как перегрузка оператора, поэтому ++ (до или после) никогда не является вызовом функции; дело не столько в том, что встраивание гарантировано, сколько в том, что в первую очередь нет ничего, что предположительно "не соответствовало бы". Обе версии ++ делают одно и то же: увеличивают числовой тип или тип указателя. pre- vs. post- просто определяет, будет ли это сделано до или после того, как значение будет использовано в выражении, которое его содержит, но копирование не требуется; для переменной всегда существует одно согласованное значение с приращением до или после точки использования.

ShadowRanger 02.05.2018 17:35

C++ не может дать эту гарантию, потому что перегрузка оператора означает, что в игру вступает семантика вызова функций; разница между ++x и x++ заключается в том, какая функция вызывается (operator++() или operator++(int)), а не в когда, которая вызывается. Если бы в стандарте C++ говорилось, что он определяет только operator++(), и просто говорилось, что он будет вызываться до или после выражения, в котором он был найден в зависимости от порядка, C++ будет соответствовать поведению C (потому что вы использовали бы только не копирующее приращение функция с синхронизацией, контролируемой компилятором), но это не так, поэтому вы можете избежать копирования только при встраивании.

ShadowRanger 02.05.2018 17:40

@ PedroLamarão: для std::atomic (с целочисленным типом и типом указателя) обе перегрузки инкремента возвращаются по значению, а не по ссылке (обычно до ref), но они возвращают неатомарное значение, поэтому «копия» бесплатна. Скорее всего, оба несут одинаковые затраты; функционально это одна и та же операция (fetch_add, который возвращает исходное значение), где предварительное приращение возвращает результат fetch_add плюс 1, а постинкремент возвращает результат fetch_add напрямую. По сравнению с накладными расходами даже на поддерживаемые ЦП атомарные добавления, неатомарное приращение результата fetch_add будет потеряно в шуме.

ShadowRanger 02.05.2018 17:48

Да. Есть.

Оператор ++ может быть определен как функция, а может и не быть. Для примитивных типов (int, double, ...) операторы встроены, поэтому компилятор, вероятно, сможет оптимизировать ваш код. Но в случае объекта, который определяет оператор ++, все обстоит иначе.

Функция operator ++ (int) должна создавать копию. Это потому, что ожидается, что postfix ++ вернет значение, отличное от того, которое он содержит: он должен хранить свое значение в переменной temp, увеличивать его значение и возвращать temp. В случае оператора ++ (), префикса ++, нет необходимости создавать копию: объект может увеличивать себя, а затем просто возвращать себя.

Вот иллюстрация к делу:

struct C
{
    C& operator++();      // prefix
    C  operator++(int);   // postfix

private:

    int i_;
};

C& C::operator++()
{
    ++i_;
    return *this;   // self, no copy created
}

C C::operator++(int ignored_dummy_value)
{
    C t(*this);
    ++(*this);
    return t;   // return a copy
}

Каждый раз, когда вы вызываете operator ++ (int), вы должны создавать копию, и компилятор ничего не может с этим поделать. Если у вас есть выбор, используйте operator ++ (); таким образом вы не сохраняете копию. Это может быть значительным в случае большого количества приращений (большой цикл?) И / или больших объектов.

«Оператор предварительного приращения вводит зависимость данных в коде: ЦП должен дождаться завершения операции приращения, прежде чем его значение можно будет использовать в выражении. В ЦП с глубоким конвейером это приводит к задержке. для оператора приращения поста ". (Архитектура игрового движка (2-е издание)) Итак, если копия пост-инкремента не требует больших вычислительных ресурсов, она все равно может превзойти пре-инкремент.

Matthias 09.10.2017 21:18

В постфиксном коде, как это работает? C t(*this); ++(*this); return t; Во второй строке вы увеличиваете указатель this вправо, так как же обновляется t, если вы увеличиваете это значение. Разве значения этого уже не скопированы в t?

rasen58 11.11.2017 06:37

The operator++(int) function must create a copy. нет, это не так. Не больше копий, чем operator++()

Severin Pappadeux 08.04.2019 01:29

Не совсем правильно сказать, что компилятор не может оптимизировать копию временной переменной в случае постфикса. Быстрый тест с VC показывает, что он, по крайней мере, может это сделать в определенных случаях.

В следующем примере сгенерированный код идентичен для префикса и постфикса, например:

#include <stdio.h>

class Foo
{
public:

    Foo() { myData=0; }
    Foo(const Foo &rhs) { myData=rhs.myData; }

    const Foo& operator++()
    {
        this->myData++;
        return *this;
    }

    const Foo operator++(int)
    {
        Foo tmp(*this);
        this->myData++;
        return tmp;
    }

    int GetData() { return myData; }

private:

    int myData;
};

int main(int argc, char* argv[])
{
    Foo testFoo;

    int count;
    printf("Enter loop count: ");
    scanf("%d", &count);

    for(int i=0; i<count; i++)
    {
        testFoo++;
    }

    printf("Value: %d\n", testFoo.GetData());
}

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

for(int i=0; i<10; i++)
{
    testFoo++;
}

printf("Value: %d\n", testFoo.GetData());

В результате получилось следующее:

00401000  push        0Ah  
00401002  push        offset string "Value: %d\n" (402104h) 
00401007  call        dword ptr [__imp__printf (4020A0h)] 

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

Вы забыли отметить важный момент, что здесь все встроено. Если определения операторов недоступны, нельзя избежать копирования, сделанного в автономном коде; с встраиванием optim совершенно очевидно, поэтому любой компилятор сделает это.

Blaisorblade 15.01.2009 03:01

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

Извините, но это меня беспокоит. Кто сказал, что это «хорошая привычка», когда это почти не имеет значения? Если люди хотят сделать это частью своей дисциплины, это нормально, но давайте отделим важные причины от вопросов личного вкуса.

Mike Dunlavey 24.08.2009 15:26

@MikeDunlavey, хорошо, какую сторону вы обычно используете, когда это не имеет значения? xD то либо одно, либо другое не так ли! сообщение ++ (если вы используете его с общим смыслом. обновите его, верните старое) полностью уступает ++ pre (обновите его, верните), нет никаких причин, по которым вы хотели бы иметь меньшую производительность. в случае, если вы захотите обновить его позже, тогда программист даже не сделает post ++. не тратить время на копирование, когда оно у нас уже есть. обновите его после того, как мы его используем. затем у компиляторов есть здравый смысл, который вы хотели.

Puddle 21.12.2018 18:04

@Puddle: Когда я слышу это: «Нет никаких причин, по которым вы хотели бы иметь меньшую производительность», я знаю, что слышу «копейки - глупые фунты». Вы должны иметь представление о вовлеченных величинах. Только если на это уходит более 1% времени, стоит даже подумать об этом. Обычно, если вы думаете об этом, нет рассматривает проблемы в миллион раз большего размера, и именно это делает программное обеспечение намного медленнее, чем могло бы быть.

Mike Dunlavey 24.12.2018 05:28

@MikeDunlavey изрыгнул ерунду, чтобы удовлетворить свое эго. вы пытаетесь походить на какого-то мудрого монаха, но ничего не говорите. вовлеченные величины ... если только более 1% времени вам следует заботиться ... xD абсолютное ведение. если это неэффективно, об этом стоит знать и исправить. мы здесь и размышляем об этом именно по этой причине! нас не беспокоит, сколько мы можем извлечь из этого знания. и когда я сказал, что вам не нужна меньшая производительность, давай, объясни тогда один чертов сценарий. МИСТЕР УАЙЗ!

Puddle 24.12.2018 18:33

@wilhelmtell

Компилятор может исключить временное. Дословно из другой ветки:

Компилятору C++ разрешено исключать временные библиотеки на основе стека, даже если это изменяет поведение программы. Ссылка MSDN для VC 8:

http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx

Это не актуально. NRVO избегает необходимости копировать t в "C C :: operator ++ (int)" обратно вызывающей стороне, но i ++ по-прежнему копирует старое значение в стек вызывающей стороны. Без NRVO i ++ создает 2 копии, одну для t и одну обратно для вызывающей стороны.

Blaisorblade 15.01.2009 03:09

Руководство по стилю Google C++ говорит:

Preincrement and Predecrement

Use prefix form (++i) of the increment and decrement operators with iterators and other template objects.

Definition: When a variable is incremented (++i or i++) or decremented (--i or i--) and the value of the expression is not used, one must decide whether to preincrement (decrement) or postincrement (decrement).

Pros: When the return value is ignored, the "pre" form (++i) is never less efficient than the "post" form (i++), and is often more efficient. This is because post-increment (or decrement) requires a copy of i to be made, which is the value of the expression. If i is an iterator or other non-scalar type, copying i could be expensive. Since the two types of increment behave the same when the value is ignored, why not just always pre-increment?

Cons: The tradition developed, in C, of using post-increment when the expression value is not used, especially in for loops. Some find post-increment easier to read, since the "subject" (i) precedes the "verb" (++), just like in English.

Decision: For simple scalar (non-object) values there is no reason to prefer one form and we allow either. For iterators and other template types, use pre-increment.

«Решение: для простых скалярных (не объектных) значений нет причин отдавать предпочтение одной форме, и мы разрешаем любую из них. Для итераторов и других типов шаблонов используйте предварительное приращение».

Nosredna 28.11.2009 20:41

Эх, ... а это что-то?

Sebastian Mach 15.11.2014 13:06

Указанная ссылка в ответе на данный момент не работает

karol 12.07.2019 10:12

Я хотел бы отметить отличный пост Эндрю Кенига о Code Talk совсем недавно.

http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29

В нашей компании мы также используем соглашение ++ iter для согласованности и производительности там, где это применимо. Но Эндрю поднимает упущенные детали относительно намерений и производительности. Бывают случаи, когда мы хотим использовать iter ++ вместо ++ iter.

Итак, сначала определитесь с вашим намерением, и если pre или post не имеют значения, тогда переходите к pre, так как это будет иметь некоторое преимущество в производительности, избегая создания дополнительного объекта и его выброса.

Обновленная ссылка: drdobbs.com/architecture-and-design/efficiency-versus-intent‌ /…

Ofek Shilon 04.05.2017 08:47

@Ketan

...raises over-looked detail regarding intent vs performance. There are times when we want to use iter++ instead of ++iter.

Очевидно, что post и pre-increment имеют разную семантику, и я уверен, что все согласны с тем, что при использовании результата вы должны использовать соответствующий оператор. Думаю, вопрос в том, что делать, когда результат отбрасывается (как в циклах for). Ответ на вопрос это (IMHO) заключается в том, что, поскольку соображения производительности в лучшем случае незначительны, вы должны делать то, что более естественно. Для меня ++i более естественен, но мой опыт подсказывает мне, что я в меньшинстве, и использование i++ вызовет меньше металлических накладных расходов для людей наиболее, читающих ваш код.

В конце концов, именно по этой причине язык не называется "++C". [*]

[*] Вставьте обязательное обсуждение того, что ++C является более логичным именем.

@Motti: (шутит) Имя C++ логично, если вы помните, как Бьярн Страуструп. C++ изначально закодировал его как прекомпилятор, генерирующий программу на C. Следовательно, C++ вернул старое значение C. Или это может быть для улучшения того, что C++ с самого начала имеет несколько концептуальных изъянов.

kriss 31.03.2010 15:37

Марк: Просто хотел указать, что операторы ++ являются хорошими кандидатами для встраивания, и если компилятор решит это сделать, избыточная копия будет удалена в большинстве случаев. (например, типы POD, которые обычно бывают итераторами.)

Тем не менее, в большинстве случаев лучше использовать ++ iter. :-)

Предполагаемый вопрос заключался в том, когда результат не используется (это ясно из вопроса для C). Может ли кто-нибудь исправить это, раз уж вопрос - "вики сообщества"?

По поводу преждевременных оптимизаций часто цитируют Кнута. Верно. но Дональд Кнут никогда не стал бы защищать этот ужасный кодекс, который вы видите в наши дни. Вы когда-нибудь видели a = b + c среди целых чисел Java (не int)? Это составляет 3 преобразования упаковки / распаковки. Важно избегать подобных вещей. И напрасно писать i ++ вместо ++ i - та же ошибка. Обновлено: Как хорошо сказал Френель в комментарии, это можно резюмировать как «преждевременная оптимизация - это зло, как и преждевременная пессимизация».

Даже тот факт, что люди больше привыкли к i ++, является неудачным наследием C, вызванным концептуальной ошибкой K&R (если вы последуете аргументу о намерениях, это логический вывод; и защищать K&R, потому что они K&R бессмысленно, они отлично, но они не так хороши как дизайнеры языка; существует бесчисленное количество ошибок в дизайне C, начиная от gets () и заканчивая strcpy (), и заканчивая API strncpy () (он должен был иметь API strlcpy () с первого дня) ).

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

Я вижу, вы работаете над докторской степенью. с интересом к оптимизации компилятора и тому подобному. Это здорово, но не забывайте, что академия - это эхо-камера, и здравый смысл часто остается за дверью, по крайней мере, в C.S. Вас может заинтересовать это: stackoverflow.com/questions/1303899/…

Mike Dunlavey 23.08.2009 22:28

Я никогда не находил ++i более раздражающим, чем i++ (на самом деле, я нашел его круче), но остальная часть вашего сообщения заслуживает моего полного признания. Может быть, добавить пункт «преждевременная оптимизация - зло, как и преждевременная пессимизация»

Sebastian Mach 01.03.2012 19:50

strncpy служил определенной цели в файловых системах, которые они использовали в то время; имя файла было 8-символьным буфером, и оно не должно было заканчиваться нулем. Вы не можете винить их в том, что они не видели 40-летнего будущего развития языка.

M.M 03.09.2014 02:15

@MattMcNabb: разве 8-значное имя файла не являлось эксклюзивным для MS-DOS? C был изобретен с помощью Unix. В любом случае, даже если strncpy имел смысл, отсутствие strlcpy не было полностью оправдано: даже в оригинальном C были массивы, которые вы не должны переполнять, а для этого требовалась strlcpy; в лучшем случае это были просто отсутствующие злоумышленники, намеревающиеся воспользоваться ошибками. Но нельзя сказать, что прогнозирование этой проблемы было тривиальным, поэтому, если бы я переписал свой пост, я бы не использовал тот же тон.

Blaisorblade 03.09.2014 21:37

@Blaisorblade: Насколько я помню, ранние имена файлов UNIX были ограничены 14 символами. Отсутствие strlcpy() было оправдано тем, что его еще не изобрели.

Keith Thompson 20.04.2015 22:12

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

Обычный ответ: ++ i быстрее, чем i ++, и, без сомнения, так оно и есть, но более серьезный вопрос - «когда вам это нужно?»

Если доля процессорного времени, затрачиваемая на увеличение итераторов, составляет менее 10%, вам может быть все равно.

Если доля времени ЦП, затрачиваемая на увеличение итераторов, превышает 10%, вы можете посмотреть, какие операторы выполняют эту итерацию. Посмотрите, можно ли просто увеличивать целые числа, а не использовать итераторы. Скорее всего, вы могли бы, и хотя это может быть в некотором смысле менее желательным, скорее всего, вы сэкономите практически все время, потраченное на эти итераторы.

Я видел пример, в котором увеличение итератора занимало более 90% времени. В этом случае переход к целочисленному приращению сокращает время выполнения по существу на эту величину. (т.е. лучше, чем 10-кратное ускорение)

Оба такие же быстрые;) Если вы хотите, чтобы это был такой же расчет для процессора, отличается только порядок, в котором он выполняется.

Например, такой код:

#include <stdio.h>

int main()
{
    int a = 0;
    a++;
    int b = 0;
    ++b;
    return 0;
}

Производим следующую сборку:

 0x0000000100000f24 <main+0>: push   %rbp
 0x0000000100000f25 <main+1>: mov    %rsp,%rbp
 0x0000000100000f28 <main+4>: movl   $0x0,-0x4(%rbp)
 0x0000000100000f2f <main+11>:    incl   -0x4(%rbp)
 0x0000000100000f32 <main+14>:    movl   $0x0,-0x8(%rbp)
 0x0000000100000f39 <main+21>:    incl   -0x8(%rbp)
 0x0000000100000f3c <main+24>:    mov    $0x0,%eax
 0x0000000100000f41 <main+29>:    leaveq 
 0x0000000100000f42 <main+30>:    retq

Вы видите, что для a ++ и b ++ это мнемоника incl, поэтому это одна и та же операция;)

Это C, в то время как OP спросил C++. В C то же самое. В C++ быстрее ++ i; из-за своего объекта. Однако некоторые компиляторы могут оптимизировать оператор постинкремента.

Wiggler Jtag 26.02.2015 00:13

Вот эталон для случая, когда операторы приращения находятся в разных единицах перевода. Компилятор с g ++ 4.5.

Пока не обращайте внимания на проблемы со стилем

// a.cc
#include <ctime>
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};

int main () {
    Something s;

    for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
    std::clock_t a = clock();
    for (int i=0; i<1024*1024*30; ++i) ++s;
    a = clock() - a;

    for (int i=0; i<1024*1024*30; ++i) s++; // warm up
    std::clock_t b = clock();
    for (int i=0; i<1024*1024*30; ++i) s++;
    b = clock() - b;

    std::cout << "a = " << (a/double(CLOCKS_PER_SEC))
              << ", b = " << (b/double(CLOCKS_PER_SEC)) << '\n';
    return 0;
}

O (n) приращение

Тест

// b.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    for (auto it=data.begin(), end=data.end(); it!=end; ++it)
        ++*it;
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

Полученные результаты

Результаты (время в секундах) с g ++ 4.5 на виртуальной машине:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      1.70  2.39
-DPACKET_SIZE=50 -O3      0.59  1.00
-DPACKET_SIZE=500 -O1    10.51 13.28
-DPACKET_SIZE=500 -O3     4.28  6.82

O (1) приращение

Тест

Давайте теперь возьмем следующий файл:

// c.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

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

Полученные результаты

Результаты сейчас сильно разнятся:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      0.05   0.74
-DPACKET_SIZE=50 -O3      0.08   0.97
-DPACKET_SIZE=500 -O1     0.05   2.79
-DPACKET_SIZE=500 -O3     0.08   2.18
-DPACKET_SIZE=5000 -O3    0.07  21.90

Вывод

По производительности

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

Семантически мудрый

  • i++ говорит increment i, I am interested in the previous value, though.
  • ++i говорит increment i, I am interested in the current value или increment i, no interest in the previous value. Опять же, вы привыкнете к этому, даже если вы не сейчас.

Кнут.

Преждевременная оптимизация - корень всех зол. Как и преждевременная пессимизация.

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

chew socks 14.08.2014 19:06

Мне бы очень хотелось увидеть реальный пример, где ++ i / i ++ имеет значение. Например, имеет ли это значение для любого из итераторов std?

Jakob Schou Jensen 04.06.2015 09:58

@JakobSchouJensen: Они предназначались для реальных примеров. Рассмотрим большое приложение со сложными древовидными структурами (например, kd-деревья, квад-деревья) или большими контейнерами, используемыми в шаблонах выражений (чтобы максимизировать пропускную способность данных на оборудовании SIMD). Если это имеет значение, я не совсем уверен, почему можно было бы вернуться к постинкременту для конкретных случаев, если это не требуется с семантической точки зрения.

Sebastian Mach 05.06.2015 11:31

@phresnel: Я не думаю, что оператор ++ в вашей повседневной жизни является шаблоном выражения - у вас есть реальный пример этого? Обычно оператор ++ используется для целых чисел и итераторов. Это было, я думаю, было бы интересно узнать, есть ли какая-нибудь разница (конечно, нет разницы в целых числах, но итераторах).

Jakob Schou Jensen 06.06.2015 18:35

@JakobSchouJensen: Нет реального бизнес-примера, но есть приложения для обработки чисел, в которых вы считаете вещи. Что касается итераторов, рассмотрите трассировщик лучей, написанный в идиоматическом стиле C++, и у вас есть итератор для обхода в глубину, такой как for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }, не говоря уже о фактической структуре дерева (BSP, kd, Quadtree, Octree Grid и т. д.). Такой итератор должен поддерживать некоторое состояние, например parent node, child node, index и тому подобное. В общем, моя позиция, даже если существует лишь несколько примеров, ...

Sebastian Mach 07.06.2015 23:16

пост инкремент, если вы имеете в виду, и используйте предварительный инкремент, если вы это имеете в виду (но я уверен, что вы не ставите под сомнение это :))

Sebastian Mach 07.06.2015 23:16

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

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

int& int::operator++() { 
     return *this += 1;
}

Но этого нельзя сказать о i++.

Постинкремент, i++, часто рассматривается как возвращающий исходное значение перед приращения. Однако функция может вернуть результат только после завершения. В результате возникает необходимость создать копию переменной, содержащей исходное значение, увеличить переменную, а затем вернуть копию, содержащую исходное значение:

int int::operator++(int& _Val) {
    int _Original = _Val;
    _Val += 1;
    return _Original;
}

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

++i быстрее, чем i++, потому что он не возвращает старую копию значения.

Это также более интуитивно понятно:

x = i++;  // x contains the old value of i
y = ++i;  // y contains the new value of i 

Этот пример C печатает "02" вместо ожидаемого числа "12":

#include <stdio.h>

int main(){
    int a = 0;
    printf("%d", a++);
    printf("%d", ++a);
    return 0;
}

То же самое для C++:

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}

Я не думаю, что ответ (э-э) имеет какое-либо представление о том, чего хочет оператор, или о том, что означает слово «быстрее».

Loves Probability 12.12.2020 19:27
  1. ++ я - быстрее не используя возвращаемое значение
  2. я ++ - быстрее с помощью возвращаемое значение

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

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

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

По сути, уловка состоит в том, чтобы использовать вспомогательный класс, чтобы отложить инкремент после возврата, и RAII приходит на помощь

#include <iostream>

class Data {
    private: class DataIncrementer {
        private: Data& _dref;

        public: DataIncrementer(Data& d) : _dref(d) {}

        public: ~DataIncrementer() {
            ++_dref;
        }
    };

    private: int _data;

    public: Data() : _data{0} {}

    public: Data(int d) : _data{d} {}

    public: Data(const Data& d) : _data{ d._data } {}

    public: Data& operator=(const Data& d) {
        _data = d._data;
        return *this;
    }

    public: ~Data() {}

    public: Data& operator++() { // prefix
        ++_data;
        return *this;
    }

    public: Data operator++(int) { // postfix
        DataIncrementer t(*this);
        return *this;
    }

    public: operator int() {
        return _data;
    }
};

int
main() {
    Data d(1);

    std::cout <<   d << '\n';
    std::cout << ++d << '\n';
    std::cout <<   d++ << '\n';
    std::cout << d << '\n';

    return 0;
}

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

++i быстрее, чем i = i +1, потому что в i = i + 1 выполняются две операции: первое приращение и второе присваивание переменной. Но в i++ выполняется только операция приращения.

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