Kami memiliki pertanyaan apakah ada perbedaan kinerja antara i++
dan ++i
di C ?
Apa jawaban untuk C ++?
Kami memiliki pertanyaan apakah ada perbedaan kinerja antara i++
dan ++i
di C ?
Apa jawaban untuk C ++?
Jawaban:
[Ringkasan Eksekutif: Gunakan ++i
jika Anda tidak memiliki alasan khusus untuk digunakan i++
.]
Untuk C ++, jawabannya sedikit lebih rumit.
Jika i
adalah tipe sederhana (bukan turunan dari kelas C ++), maka jawaban yang diberikan untuk C ("Tidak ada perbedaan kinerja") berlaku, karena kompiler menghasilkan kode.
Namun, jika i
adalah turunan dari kelas C ++, maka i++
dan ++i
membuat panggilan ke salah satu operator++
fungsi. Berikut ini pasangan standar dari fungsi-fungsi ini:
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;
}
Karena kompiler tidak menghasilkan kode, tetapi hanya memanggil operator++
fungsi, tidak ada cara untuk mengoptimalkan tmp
variabel dan penyalin salinan yang terkait. Jika pembuat salinan mahal, maka ini dapat memiliki dampak kinerja yang signifikan.
Iya. Ada.
Operator ++ mungkin atau mungkin tidak didefinisikan sebagai suatu fungsi. Untuk tipe primitif (int, dobel, ...) operator sudah terpasang, sehingga kompiler mungkin akan dapat mengoptimalkan kode Anda. Tetapi dalam kasus objek yang mendefinisikan operator ++ hal-hal berbeda.
Fungsi operator ++ (int) harus membuat salinan. Itu karena postfix ++ diharapkan mengembalikan nilai yang berbeda dari yang dipegangnya: postfix ++ harus menyimpan nilainya dalam variabel temp, menambah nilainya dan mengembalikan temp. Dalam kasus operator ++ (), awalan ++, tidak perlu membuat salinan: objek dapat bertambah dengan sendirinya dan kemudian kembali sendiri.
Inilah ilustrasi pokoknya:
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
}
Setiap kali Anda memanggil operator ++ (int) Anda harus membuat salinan, dan kompiler tidak dapat melakukan apa-apa. Saat diberi pilihan, gunakan operator ++ (); dengan cara ini Anda tidak menyimpan salinan. Mungkin signifikan dalam kasus banyak kenaikan (loop besar?) Dan / atau objek besar.
C t(*this); ++(*this); return t;
Di baris kedua, Anda menambah pointer ini dengan benar, jadi bagaimana cara t
diperbarui jika Anda menambah ini. Bukankah nilai-nilai ini sudah disalin t
?
The operator++(int) function must create a copy.
Tidak, bukan itu. Tidak lebih dari salinanoperator++()
Berikut ini adalah patokan untuk kasus ketika operator tambahan berada di unit terjemahan berbeda Kompiler dengan g ++ 4.5.
Abaikan masalah gaya untuk saat ini
// 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;
}
Hasil (timing dalam detik) dengan g ++ 4.5 pada mesin virtual:
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
Sekarang mari kita ambil file berikut:
// 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;
}
Itu tidak melakukan apa-apa dalam penambahan. Ini mensimulasikan kasus ketika peningkatan memiliki kompleksitas konstan.
Hasil sekarang sangat bervariasi:
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
Jika Anda tidak membutuhkan nilai sebelumnya, biasakan untuk menggunakan pra-kenaikan. Konsisten bahkan dengan tipe builtin, Anda akan terbiasa dan tidak berisiko kehilangan kinerja yang tidak perlu jika Anda pernah mengganti tipe builtin dengan tipe kustom.
i++
kata increment i, I am interested in the previous value, though
.++i
kata increment i, I am interested in the current value
atau increment i, no interest in the previous value
. Sekali lagi, Anda akan terbiasa, bahkan jika Anda tidak sekarang.Optimalisasi prematur adalah akar dari semua kejahatan. Seperti pesimisasi dini.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
, tidak peduli struktur pohon yang sebenarnya (BSP, kd, Quadtree, Octree Grid, dll.). Sebuah seperti iterator akan perlu untuk mempertahankan beberapa negara, misalnya parent node
, child node
, index
dan hal-hal seperti itu. Secara keseluruhan, sikap saya adalah, bahkan jika hanya ada beberapa contoh, ...
Tidak sepenuhnya benar untuk mengatakan bahwa kompiler tidak dapat mengoptimalkan salinan variabel sementara dalam kasus postfix. Tes cepat dengan VC menunjukkan bahwa itu, setidaknya, dapat melakukannya dalam kasus-kasus tertentu.
Dalam contoh berikut, kode yang dihasilkan identik untuk awalan dan postfix, misalnya:
#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());
}
Apakah Anda melakukan ++ testFoo atau testFoo ++, Anda akan tetap mendapatkan kode hasil yang sama. Bahkan, tanpa membaca hitungan dari pengguna, pengoptimal membuat semuanya menjadi konstan. Jadi ini:
for(int i=0; i<10; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
Menghasilkan sebagai berikut:
00401000 push 0Ah
00401002 push offset string "Value: %d\n" (402104h)
00401007 call dword ptr [__imp__printf (4020A0h)]
Jadi, walaupun tentu saja versi postfix bisa lebih lambat, mungkin saja pengoptimal akan cukup baik untuk menyingkirkan salinan sementara jika Anda tidak menggunakannya.
The Google C ++ Style Guide mengatakan:
Preincrement dan Predecrement
Gunakan formulir awalan (++ i) dari operator increment dan decrement dengan iterator dan objek templat lainnya.
Definisi: Ketika suatu variabel bertambah (++ i atau i ++) atau dikurangi (--i atau i--) dan nilai dari ekspresi tidak digunakan, kita harus memutuskan apakah akan preincrement (pengurangan) atau postincrement (pengurangan).
Pro: Ketika nilai kembali diabaikan, bentuk "pra" (++ i) tidak pernah kurang efisien daripada bentuk "pos" (i ++), dan seringkali lebih efisien. Ini karena post-increment (atau decrement) membutuhkan salinan i untuk dibuat, yang merupakan nilai dari ekspresi. Jika saya seorang iterator atau tipe non-skalar lainnya, menyalin saya bisa jadi mahal. Karena dua jenis kenaikan berperilaku sama ketika nilainya diabaikan, mengapa tidak selalu selalu pra-kenaikan?
Cons: Tradisi dikembangkan, dalam C, menggunakan post-increment ketika nilai ekspresi tidak digunakan, terutama untuk loop. Beberapa menemukan post-increment lebih mudah dibaca, karena "subjek" (i) mendahului "kata kerja" (++), sama seperti dalam bahasa Inggris.
Keputusan: Untuk nilai skalar (non-objek) sederhana, tidak ada alasan untuk memilih satu bentuk dan kami mengizinkan keduanya. Untuk iterator dan tipe templat lainnya, gunakan pra-kenaikan.
Saya ingin menunjukkan posting yang sangat baik oleh Andrew Koenig pada Code Talk baru-baru ini.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
Di perusahaan kami juga kami menggunakan konvensi ++ iter untuk konsistensi dan kinerja di mana berlaku. Namun Andrew memunculkan detail yang terlalu banyak terkait dengan niat vs kinerja. Ada kalanya kita ingin menggunakan iter ++ daripada ++ iter.
Jadi, pertama-tama tentukan niat Anda dan jika pra atau posting tidak masalah, maka pergi dengan pra karena akan memiliki beberapa manfaat kinerja dengan menghindari penciptaan objek tambahan dan melemparkannya.
@Ketan
... memunculkan detail yang kelihatannya terkait dengan niat vs kinerja. Ada kalanya kita ingin menggunakan iter ++ daripada ++ iter.
Jelas posting dan pra-kenaikan memiliki semantik yang berbeda dan saya yakin semua orang setuju bahwa ketika hasilnya digunakan, Anda harus menggunakan operator yang sesuai. Saya pikir pertanyaannya adalah apa yang harus dilakukan ketika hasilnya dibuang (seperti dalam for
loop). Jawaban untuk pertanyaan ini (IMHO) adalah bahwa, karena pertimbangan kinerja dapat diabaikan, Anda harus melakukan apa yang lebih alami. Bagi saya sendiri ++i
lebih alami tetapi pengalaman saya memberi tahu saya bahwa saya adalah minoritas dan menggunakan i++
akan menyebabkan lebih sedikit overhead logam bagi kebanyakan orang membaca kode Anda.
Lagipula itulah alasan mengapa bahasa itu tidak disebut " ++C
".
[*] Masukkan diskusi wajib tentang ++C
menjadi nama yang lebih logis.
Ketika tidak menggunakan nilai kembali kompiler dijamin tidak menggunakan sementara dalam kasus ++ i . Tidak dijamin lebih cepat, tapi dijamin tidak lebih lambat.
Saat menggunakan nilai balik i ++ memungkinkan prosesor untuk mendorong kenaikan dan sisi kiri ke dalam pipa karena keduanya tidak saling bergantung. ++ i dapat menghentikan pipa karena prosesor tidak dapat memulai sisi kiri sampai operasi pra-kenaikan telah berkelok-kelok sepanjang jalan. Sekali lagi, warung pipa tidak dijamin, karena prosesor dapat menemukan hal-hal berguna lainnya untuk ditempelkan.
Mark: Hanya ingin menunjukkan bahwa operator ++ adalah kandidat yang baik untuk diuraikan, dan jika kompiler memilih untuk melakukannya, salinan yang berlebihan akan dihilangkan dalam banyak kasus. (mis. tipe POD, yang biasanya merupakan iterator.)
Yang mengatakan, itu masih gaya yang lebih baik untuk menggunakan ++ iter dalam banyak kasus. :-)
Perbedaan kinerja antara ++i
dan i++
akan lebih jelas ketika Anda menganggap operator sebagai fungsi pengembalian nilai dan bagaimana mereka diimplementasikan. Untuk membuatnya lebih mudah untuk memahami apa yang terjadi, contoh kode berikut akan digunakan int
seolah-olah itu adalah a struct
.
++i
menambah variabel, lalu mengembalikan hasilnya. Ini dapat dilakukan di tempat dan dengan waktu CPU minimal, hanya membutuhkan satu baris kode dalam banyak kasus:
int& int::operator++() {
return *this += 1;
}
Tetapi hal yang sama tidak bisa dikatakan i++
.
Pasca-kenaikan,, i++
sering dianggap mengembalikan nilai asli sebelum bertambah. Namun, fungsi hanya dapat mengembalikan hasil ketika selesai . Akibatnya, menjadi perlu untuk membuat salinan variabel yang berisi nilai asli, menambah variabel, lalu mengembalikan salinan yang menyimpan nilai asli:
int int::operator++(int& _Val) {
int _Original = _Val;
_Val += 1;
return _Original;
}
Ketika tidak ada perbedaan fungsional antara pra-kenaikan dan pasca-kenaikan, kompiler dapat melakukan optimasi sehingga tidak ada perbedaan kinerja antara keduanya. Namun, jika tipe data komposit seperti struct
atau class
terlibat, copy constructor akan dipanggil pasca kenaikan, dan itu tidak akan mungkin untuk melakukan optimasi ini jika diperlukan salinan yang dalam. Dengan demikian, pre-increment umumnya lebih cepat dan membutuhkan lebih sedikit memori daripada post-increment.
@ Mark: Saya menghapus jawaban saya sebelumnya karena itu sedikit terbalik, dan pantas menerima downvote untuk itu saja. Saya benar-benar berpikir itu adalah pertanyaan yang bagus dalam arti bahwa itu menanyakan apa yang ada dalam pikiran banyak orang.
Jawaban yang biasa adalah bahwa ++ i lebih cepat dari i ++, dan tidak diragukan lagi, tapi pertanyaan yang lebih besar adalah "kapan Anda peduli?"
Jika fraksi waktu CPU yang dihabiskan untuk meningkatkan iterator kurang dari 10%, maka Anda mungkin tidak peduli.
Jika fraksi waktu CPU yang dihabiskan untuk meningkatkan iterator lebih besar dari 10%, Anda dapat melihat pernyataan mana yang melakukan iterasi. Lihat apakah Anda bisa menambah bilangan bulat daripada menggunakan iterator. Kemungkinannya adalah Anda bisa, dan sementara itu mungkin dalam beberapa hal kurang diinginkan, kemungkinan cukup bagus Anda akan menghemat dasarnya semua waktu yang dihabiskan di iterator tersebut.
Saya telah melihat contoh di mana iterator-incrementing menghabiskan lebih dari 90% waktu. Dalam hal itu, penambahan bilangan bulat akan mengurangi waktu eksekusi pada dasarnya jumlah itu. (Yaitu lebih baik dari 10x speedup)
@wilhelmtell
Compiler dapat menghilangkan sementara. Verbatim dari utas lainnya:
Kompilator C ++ diizinkan untuk menghilangkan temporari berbasis stack meskipun jika itu mengubah perilaku program. Tautan MSDN untuk VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
Alasan mengapa Anda harus menggunakan ++ i bahkan pada tipe bawaan di mana tidak ada keuntungan kinerja adalah untuk menciptakan kebiasaan yang baik untuk diri sendiri.
Keduanya sama cepat;) Jika Anda ingin itu adalah perhitungan yang sama untuk prosesor, itu hanya urutan yang dilakukan berbeda.
Misalnya, kode berikut:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Hasilkan perakitan berikut:
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
Anda melihat bahwa untuk ++ dan b ++ ini termasuk mnemonic, jadi ini operasi yang sama;)
Pertanyaan yang dimaksud adalah kapan hasilnya tidak digunakan (itu jelas dari pertanyaan untuk C). Adakah yang bisa memperbaikinya karena pertanyaannya adalah "komunitas wiki"?
Tentang optimasi prematur, Knuth sering dikutip. Betul. tetapi Donald Knuth tidak akan pernah membela dengan kode mengerikan yang dapat Anda lihat di hari-hari ini. Pernah melihat a = b + c di antara Java Integers (bukan int)? Itu berjumlah 3 konversi tinju / unboxing. Menghindari hal-hal seperti itu penting. Dan sia-sia menulis i ++ bukan ++ i adalah kesalahan yang sama. EDIT: Seperti yang dikatakan baik oleh phresnel, komentar ini dapat disimpulkan sebagai "optimisasi prematur itu jahat, seperti pesimisasi prematur".
Bahkan fakta bahwa orang lebih terbiasa dengan i ++ adalah warisan C yang tidak menguntungkan, disebabkan oleh kesalahan konseptual oleh K&R (jika Anda mengikuti argumen maksud, itu kesimpulan logis; dan membela K&R karena mereka K&R tidak ada artinya, mereka hebat, tetapi mereka tidak hebat sebagai perancang bahasa; banyak kesalahan dalam desain C ada, mulai dari get () hingga strcpy (), hingga strncpy () API (seharusnya memiliki strlcpy () API sejak hari 1) ).
Btw, saya salah satu dari mereka tidak cukup digunakan untuk C ++ untuk menemukan ++ saya mengganggu untuk dibaca. Namun, saya menggunakannya karena saya mengakui bahwa itu benar.
++i
lebih menyebalkan daripada i++
(pada kenyataannya, saya merasa lebih keren), tetapi sisa posting Anda mendapat pengakuan penuh dari saya. Mungkin menambahkan poin "optimasi prematur itu jahat, seperti pesimisasi prematur"
strncpy
melayani tujuan dalam sistem file yang mereka gunakan pada saat itu; nama file adalah buffer 8 karakter dan tidak harus diakhiri dengan null. Anda tidak dapat menyalahkan mereka karena tidak melihat 40 tahun ke depan evolusi bahasa.
strlcpy()
dibenarkan oleh fakta bahwa itu belum ditemukan.
Saatnya memberikan permata kebijaksanaan kepada orang-orang;) - ada trik sederhana untuk membuat kenaikan C ++ postfix berperilaku hampir sama dengan kenaikan awalan (Diciptakan ini untuk saya sendiri, tetapi melihatnya juga dalam kode orang lain, jadi saya tidak sendirian).
Pada dasarnya, triknya adalah menggunakan kelas pembantu untuk menunda kenaikan setelah pengembalian, dan RAII datang untuk menyelamatkan
#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;
}
Diciptakan adalah untuk beberapa kode iterators khusus yang berat, dan mengurangi run-time. Biaya awalan vs postfix adalah salah satu referensi sekarang, dan jika ini adalah operator kustom melakukan banyak bergerak, awalan dan postfix menghasilkan run-time yang sama untuk saya.
++i
lebih cepat daripada i++
karena itu tidak mengembalikan salinan nilai yang lama.
Ini juga lebih intuitif:
x = i++; // x contains the old value of i
y = ++i; // y contains the new value of i
Contoh C ini mencetak "02" alih-alih "12" yang mungkin Anda harapkan:
#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;
}