У нас виникає питання , чи є різниця в продуктивності між 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;
}