У нас виникає питання , чи є різниця в продуктивності між i++
і ++i
в C ?
Яка відповідь на C ++?
У нас виникає питання , чи є різниця в продуктивності між i++
і ++i
в C ?
Яка відповідь на C ++?
Відповіді:
[Резюме: Використовуйте, ++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
змінну та пов'язаний з нею конструктор копій. Якщо конструктор копій дорогий, то це може мати значний вплив на продуктивність.
Так. Існує.
Оператор ++ може або не може бути визначений як функція. Для примітивних типів (int, double, ...) вбудовані оператори, тому компілятор, ймовірно, зможе оптимізувати ваш код. Але у випадку з об'єктом, який визначає оператор ++, речі відрізняються.
Оператор ++ (int) функція повинна створити копію. Це тому, що очікується, що постфікс ++ поверне інше значення, ніж те, що він містить: він повинен утримувати своє значення в змінній 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
}
Кожен раз, коли ви зателефонуєте до оператора ++ (int), ви повинні створити копію, і компілятор нічого не може з цим зробити. Якщо вам надано вибір, використовуйте оператор ++ (); таким чином ви не зберігаєте копію. Це може бути значним у випадку багатьох приростів (великий цикл?) Та / або великих об'єктів.
C t(*this); ++(*this); return t;
У другому рядку ви збільшуєте цей покажчик праворуч, і як t
оновиться, якщо ви збільшуєте це. Чи не були вже скопійовані значення цього t
?
The operator++(int) function must create a copy.
Ні, це не. Не більше копій ніжoperator++()
Ось орієнтир для випадку, коли оператори приросту знаходяться в різних одиницях перекладу. Компілятор з 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;
}
// 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
Тепер візьмемо такий файл:
// 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
. Знову ви звикнете, навіть якщо ви зараз не правий.Передчасна оптимізація - корінь усього зла. Як і передчасна песимізація.
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
тощо. Загалом, моя позиція є, навіть якщо існує лише кілька прикладів, ...
Не зовсім коректно сказати, що компілятор не може оптимізувати тимчасову копію змінної у випадку postfix. Швидкий тест з 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)]
Тож, безумовно, випадок, що версія postfix може бути повільнішою, можливо, оптимізатор буде досить хорошим, щоб позбутися від тимчасової копії, якщо ви не використовуєте її.
В Google C ++ Style Guide каже:
Підвищення та попереднє посилення
Використовуйте форму префікса (++ i) операторів збільшення та зменшення з ітераторами та іншими об'єктами шаблону.
Визначення: Коли змінна збільшується (++ i або i ++) або зменшується (--i або i--) і значення виразу не використовується, слід вирішити, чи слід попередньо збільшувати (декремент) чи постінкремент (декремент).
Плюси: Коли значення повернення ігнорується, форма "до" (++ i) ніколи не є менш ефективною, ніж форма "пост" (i ++), і часто є більш ефективною. Це відбувається тому, що для посилення (або зменшення) потрібна копія i, яка є значенням виразу. Якщо я є ітератором чи іншим не скалярним типом, копіювання я могло б бути дорогим. Оскільки два типи приросту поводяться однаково, коли значення ігнорується, чому б не просто завжди попередньо збільшувати?
Мінуси: В Створена традиція використовувати пост-приріст, коли значення виразу не використовується, особливо для циклів. Дехто вважає, що посткраст легше читати, оскільки "тема" (i) передує "дієслову" (++), як і англійською мовою.
Рішення: Для простих скалярних (не об’єктних) значень немає причин віддавати перевагу одній формі, і ми допускаємо будь-яку. Для ітераторів та інших типів шаблонів використовуйте попередній приріст.
Я хотів би відзначити чудовий пост Ендрю Коніга на Code Talk зовсім недавно.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
У нашій компанії ми також використовуємо конвенцію ++ iter для послідовності та продуктивності, де це можливо. Але Ендрю підкреслює переглянуті деталі щодо намірів та ефективності. Бувають випадки, коли ми хочемо використовувати iter ++ замість ++ iter.
Отже, спочатку визначтесь із своїм наміром, і якщо до чи пост не має значення, тоді перейдіть заздалегідь, оскільки це матиме певну користь від продуктивності, уникаючи створення зайвого об’єкта та його викидання.
@Ketan
... піднімає завищені деталі щодо намірів та виконання. Бувають випадки, коли ми хочемо використовувати iter ++ замість ++ iter.
Очевидно, що пост і попередній приріст мають різну семантику, і я впевнений, що всі згодні, що коли використовується результат, ви повинні використовувати відповідного оператора. Я думаю, питання полягає в тому, що робити, коли результат відкидається (як у for
циклі). Відповідь на це запитання (IMHO) полягає в тому, що, оскільки міркування щодо ефективності в кращому випадку незначні, вам слід зробити те, що є більш природним. Для мене ++i
більш природно, але мій досвід говорить про те, що я в меншості, і використання i++
меншої кількості металевих накладних витрат для більшості людей, які читають ваш код.
Зрештою, це причина, яку мову не називають " ++C
". [*]
[*] Вставте обов’язкову дискусію про ++C
те, щоб бути більш логічним ім'ям.
Якщо не використовується повернене значення, компілятор гарантовано не використовує тимчасовий у випадку ++ i . Не гарантовано швидше, але гарантовано не повільніше.
При використанні зворотного значення i ++ дозволяє процесору проштовхувати як приріст, так і ліву сторону в трубопровід, оскільки вони не залежать один від одного. ++ Я можу зупинити конвеєр, тому що процесор не може запустити ліву сторону, поки операція попереднього збільшення не буде змінена до кінця. Знову ж таки, застій трубопроводу не гарантується, оскільки процесор може знайти інші корисні речі, до яких слід приклеїтись.
Марк: Просто хотілося зазначити, що оператори ++ є хорошими кандидатами, які слід накреслити, і якщо компілятор вирішить це зробити, зайва копія буде усунена в більшості випадків. (наприклад, типи POD, якими зазвичай є ітератори.)
Однак, у більшості випадків все-таки краще використовувати стиль ++. :-)
Різниця в продуктивності між ++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;
}
Коли немає функціональної різниці між попереднім збільшенням і після збільшення, компілятор може виконати оптимізацію таким чином, щоб між ними не було різниці в продуктивності. Однак якщо задіяний такий тип даних, як a struct
або class
, конструктор копій буде викликаний після інкременту, і неможливо здійснити цю оптимізацію, якщо потрібна глибока копія. Таким чином, попередній приріст, як правило, швидше і вимагає менше пам'яті, ніж після збільшення.
@Mark: Я видалив попередню відповідь, тому що вона була трохи перевернута, і заслужила знищення лише для цього. Я насправді думаю, що це гарне запитання в тому сенсі, що він запитує, що на розум багатьох людей.
Звичайна відповідь полягає в тому, що ++ i швидше, ніж i ++, і, без сумніву, це так, але більш важливим питанням є "коли вам слід піклуватися?"
Якщо частка часу процесора, витраченого на збільшення ітераторів, становить менше 10%, то вас може не хвилювати.
Якщо частка часу процесора, витраченого на збільшення ітераторів, перевищує 10%, ви можете подивитися, які заяви роблять цю ітерацію. Подивіться, чи можна просто збільшувати цілі числа, а не використовувати ітератори. Швидше за все, ви можете, і хоча це може бути в деякому сенсі менш бажаним, швидше за все, ви заощадите весь час, проведений у цих ітераторах.
Я бачив приклад, коли збільшення ітератора споживало понад 90% часу. У такому випадку перехід на ціле збільшення збільшує час виконання майже на цю суму. (тобто краще, ніж 10-кратне прискорення)
@wilhelmtell
Компілятор може схилити тимчасове. Дослівний з іншої теми:
Компілятору C ++ дозволено елімінувати тимчасові бази даних, навіть якщо це змінює поведінку програми. MSDN-посилання для VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
Причиною, чому ви повинні використовувати ++ i навіть у вбудованих типах, де немає переваги в продуктивності, - це створити собі хорошу звичку.
Обидва настільки ж швидкі;) Якщо ви хочете, що це однаковий розрахунок для процесора, це лише той порядок, в якому це робиться, що відрізняються.
Наприклад, наступний код:
#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
Ви бачите, що для ++ та b ++ це мнемонічна величина, тому це та сама операція;)
Задане питання стосувалося того, коли результат не використовується (це зрозуміло з питання для C). Хтось може це виправити, оскільки питання "вікі спільноти"?
Про передчасні оптимізації часто згадується Кнут. Це вірно. але Дональд Кнут ніколи б не захищав той жахливий код, який ви можете побачити в наші дні. Коли-небудь бачив a = b + c серед Java Integers (не int)? Це становить 3 перетворення боксу / розпакування. Важливо уникати подібних речей. І марно писати i ++ замість ++ i - це та сама помилка. EDIT: Оскільки френель гарно зазначає це в коментарі, це можна підсумувати як "передчасна оптимізація - це зло, як і передчасна песимізація".
Навіть той факт, що люди більше звикли до i ++, - це нещасна спадщина C, спричинена концептуальною помилкою K&R (якщо ви дотримуєтесь аргументу наміру, це логічний висновок; і захищати K&R, оскільки вони K&R - безглуздо, вони чудово, але вони не великі як дизайнери мови; існує безліч помилок у дизайні C, починаючи від get () до strcpy (), до API strncpy () (у нього повинен був бути API strlcpy () з 1 дня) ).
До речі, я один з тих, хто недостатньо звик C ++, щоб знайти ++, який мені дратує читати. Я все-таки використовую це, оскільки визнаю, що це правильно.
++i
більше дратівливих, ніж i++
(насправді, я вважаю, що це крутіше), але решта вашого допису отримує моє повне визнання. Можливо, додамо пункт "передчасна оптимізація - це зло, як і передчасна песимізація"
strncpy
слугував меті у файлових системах, якими вони користувались у той час; ім'я файлу було 8-символьним буфером, і його не потрібно було закінчувати нуль. Ви не можете звинувачувати їх у тому, що не бачили 40 років у майбутньому еволюції мови.
strlcpy()
була виправдана тим, що вона ще не була винайдена.
Час надати людям дорогоцінні камені мудрості;) - є простий трюк, щоб змусити приріст постфікса 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;
}
Винайдено для деяких важких спеціальних кодів ітераторів, і це скорочує час роботи. Вартість префікса vs postfix - це одна орієнтир, і якщо це звичайний оператор, який робить важкі переміщення, префікс і постфікс дають однаковий час для мене.
++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;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}