Является ли x + = a быстрее, чем x = x + a?

84

Я читал "Язык программирования C ++" Страуструпа, где он говорит, что из двух способов добавить что-то к переменной

x = x + a;

и

x += a;

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

Chiffa
источник
45
«Язык программирования C ++» был впервые опубликован в 1985 году. Самая последняя версия была опубликована в 1997 году, а специальное издание версии 1997 года было опубликовано в 2000 году. Как следствие, некоторые части сильно устарели.
JoeG
5
Эти две строки потенциально могут делать что-то совершенно другое. Вы должны быть более конкретными.
Kerrek SB
26
Современные компиляторы достаточно умны, чтобы эти вопросы считались «устаревшими».
gd1
2
Повторно открыл это, потому что повторяющийся вопрос касается C, а не C ++.
Кев

Ответы:

212

Любой достойный компилятор сгенерирует точно такую ​​же последовательность на машинном языке для обеих конструкций для любого встроенного типа ( int, floatи т. Д.), Если оператор действительно настолько прост x = x + a; и оптимизация включена . (Примечательно, что GCC -O0, который является режимом по умолчанию, выполняет антиоптимизацию , например, вставляет в память совершенно ненужные хранилища, чтобы гарантировать, что отладчики всегда могут найти значения переменных.)

Однако если утверждение более сложное, они могут быть другими. Предположим f, это функция, которая возвращает указатель, тогда

*f() += a;

звонит fтолько один раз, тогда как

*f() = *f() + a;

называет это дважды. Если fесть побочные эффекты, одно из двух будет неправильным (возможно, второе). Даже если у fнего нет побочных эффектов, компилятор не сможет устранить второй вызов, поэтому последний действительно может быть медленнее.

И поскольку мы говорим здесь о C ++, ситуация совершенно иная для типов классов, которые перегружают operator+и operator+=. Если xэто такой тип, то - до оптимизации - x += aпреобразуется в

x.operator+=(a);

тогда как x = x + aпереводится как

auto TEMP(x.operator+(a));
x.operator=(TEMP);

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

Zwol
источник
14
Есть еще один аспект - читабельность . Идиома C ++ для добавления exprв varis var+=exprи написания ее другим способом запутает читателей.
Тадеуш Копец
21
Если вы обнаружите, что пишете *f() = *f() + a;, возможно, вам стоит хорошенько взглянуть на то, чего вы действительно пытаетесь достичь ...
Адам Дэвис
3
И если var = var + expr вас смущает, а var + = expr - нет, вы самый странный инженер-программист, которого я когда-либо встречал. Либо читаются; просто убедитесь, что вы последовательны (и мы все используем op =, так что в любом случае это спорный = P)
WhozCraig
7
@PiotrDobrogost: Что не так с ответами на вопросы? В любом случае проверять дубликаты должен именно опрашивающий.
Gorpik
4
@PiotrDobrogost мне кажется, что ты немного ... завидуешь ... Если хочешь поискать дубликаты, дерзай. Я, со своей стороны, предпочитаю отвечать на вопросы, а не искать обманщиков (если только это не вопрос, который я специально помню, что видел раньше). Иногда это может быть быстрее и, следовательно, быстрее помочь тому, кто задал вопрос. Также обратите внимание, что это даже не цикл. 1является константой, aможет быть изменчивым, определяемым пользователем типом или чем-то еще. Полностью отличается. На самом деле, я не понимаю, как это вообще закрылось.
Лучиан Григоре
56

Вы можете проверить, посмотрев разборку, которая будет такой же.

Для основных типов оба одинаково быстры.

Это результат, сгенерированный отладочной сборкой (т.е. без оптимизации):

    a += x;
010813BC  mov         eax,dword ptr [a]  
010813BF  add         eax,dword ptr [x]  
010813C2  mov         dword ptr [a],eax  
    a = a + x;
010813C5  mov         eax,dword ptr [a]  
010813C8  add         eax,dword ptr [x]  
010813CB  mov         dword ptr [a],eax  

Для определяемых пользователем типов , где можно перегрузить operator +и operator +=, это зависит от их соответствующих реализаций.

Лучиан Григоре
источник
1
Не во всех случаях. Я обнаружил, что загружать адрес памяти в регистр, увеличивать его и записывать обратно может быть быстрее, чем напрямую увеличивать адрес памяти (без использования атомики). Я попробую пошуршать код ...
Джеймс
Как насчет пользовательских типов? Хорошие компиляторы должны генерировать эквивалентную сборку, но такой гарантии нет.
mfontanini
1
@LuchianGrigore Нет, если aравно 1 , и xэто volatileкомпилятор может генерировать inc DWORD PTR [x]. Это медленно.
Джеймс
1
@Chiffa не зависит от компилятора, но зависит от разработчика. Вы можете реализовать operator +ничего не делать и operator +=вычислить 100000-е простое число, а затем вернуться. Конечно, это было бы глупо, но это возможно.
Лучиан Григоре
3
@ Джеймс: если ваша программа чувствительна к разнице в производительности между ++xи temp = x + 1; x = temp;, то, скорее всего, она должна быть написана на ассемблере, а не на C ++ ...
EmirCalabuch
11

Да! Быстрее писать, быстрее читать и быстрее понимать, последнее в том случае, еслиx может иметь побочные эффекты. Так что для людей это в целом быстрее. Человеческое время в целом стоит намного больше, чем компьютерное время, так что вы, должно быть, и спрашивали об этом. Правильно?

Марк Адлер
источник
8

Это действительно зависит от типа x и a и реализации +. За

   T x, a;
   ....
   x = x + a;

компилятор должен создать временный T, содержащий значение x + a, пока он оценивает его, который затем может присвоить x. (Во время этой операции нельзя использовать x или a в качестве рабочей области).

Для x + = a временное значение не требуется.

Для тривиальных типов разницы нет.

Том Таннер
источник
8

Разница между x = x + aи x += aзаключается в объеме работы, которую должна выполнить машина - некоторые компиляторы могут (и обычно делают) оптимизировать ее, но обычно, если мы какое-то время игнорируем оптимизацию, происходит то, что в предыдущем фрагменте кода машина должна найти значение дляx дважды, в то время как в последнем случае этот поиск должен выполняться только один раз.

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

PS: Первый ответ о переполнении стека!

Сагар Ахире
источник
6

Как вы назвали этот C ++, из двух опубликованных вами утверждений невозможно узнать. Вам нужно знать, что такое «x» (это немного похоже на ответ «42»). Если xэто POD, то особой разницы в этом нет. Однако, если xэто класс, может быть перегрузками для operator +и operator +=методов , которые могут иметь различные модели поведения , которые приводят к очень разному времени исполнения.

Skizz
источник
6

Если вы говорите +=, что значительно упрощаете жизнь компилятору. Чтобы компилятор распознал, что x = x+aэто то же самое x += a, компилятор должен

  • проанализируйте левую часть ( x), чтобы убедиться, что она не имеет побочных эффектов и всегда относится к одному и тому же l-значению. Например, это может быть z[i], и он должен быть уверен, что и то, zи другое iне изменится.

  • проанализируйте правую часть ( x+a) и убедитесь, что это суммирование, и что левая часть встречается один раз и только один раз в правой части, даже если она может быть преобразована, как в z[i] = a + *(z+2*0+i).

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

Майк Данлэйви
источник
5

В качестве конкретного примера представьте себе простой тип комплексного числа:

struct complex {
    double x, y;
    complex(double _x, double _y) : x(_x), y(_y) { }
    complex& operator +=(const complex& b) {
        x += b.x;
        y += b.y;
        return *this;
    }
    complex operator +(const complex& b) {
        complex result(x+b.x, y+b.y);
        return result;
    }
    /* trivial assignment operator */
}

Для случая a = a + b он должен создать дополнительную временную переменную, а затем скопировать ее.

Случайный832
источник
это очень хороший пример, показывает, как реализованы 2 оператора.
Grijesh Chauhan
5

Вы задаете неправильный вопрос.

Это вряд ли повлияет на производительность приложения или функции. Даже если бы это было так, способ узнать это - профилировать код и точно знать, как он на вас влияет. Вместо того, чтобы беспокоиться на этом уровне о том, что быстрее, гораздо важнее думать с точки зрения ясности, правильности и удобочитаемости.

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

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

Джоэл Кохорн
источник
Конечно, но это был вопрос низкого уровня, а не большая картина «Когда я должен учитывать такую ​​разницу».
Chiffa
1
Вопрос OP был полностью законным, как показали другие ответы (и положительные отзывы). Хотя мы понимаем вашу точку зрения (сначала профиль и т. Д.), Это определенно , является интересно знать , такого рода вещь - вы действительно собираетесь профилировать каждое тривиальное утверждение , что вы пишите, профилировать результаты каждого решения вы принимаете? Даже когда на SO есть люди, которые уже изучили, профилировали, разобрали корпус и могут дать общий ответ?
4

Я думаю, это должно зависеть от машины и ее архитектуры. Если его архитектура допускает косвенную адресацию памяти, средство записи компилятора МОЖЕТ просто использовать вместо этого этот код (для оптимизации):

mov $[y],$ACC

iadd $ACC, $[i] ; i += y. WHICH MIGHT ALSO STORE IT INTO "i"

Принимая во внимание, что i = i + yможет быть переведено (без оптимизации):

mov $[i],$ACC

mov $[y],$B 

iadd $ACC,$B

mov $B,[i]


При этом iследует учитывать и другие сложности, например, если функция возвращает указатель и т. Д. Большинство компиляторов производственного уровня, включая GCC, создают один и тот же код для обоих операторов (если они целые).

Аникет Инге
источник
2

Нет, оба варианта одинаковы.

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