Berapa biaya overhead dari pointer pintar dibandingkan dengan pointer normal di C ++?

101

Berapa biaya overhead dari pointer pintar dibandingkan dengan pointer normal di C ++ 11? Dengan kata lain, apakah kode saya akan menjadi lebih lambat jika saya menggunakan petunjuk cerdas, dan jika demikian, seberapa lambat?

Secara khusus, saya bertanya tentang C ++ 11 std::shared_ptrdan std::unique_ptr.

Jelas, barang-barang yang didorong ke bawah tumpukan akan menjadi lebih besar (setidaknya saya kira begitu), karena penunjuk cerdas juga perlu menyimpan keadaan internalnya (jumlah referensi, dll), pertanyaan sebenarnya adalah, berapa ini akan terjadi? mempengaruhi kinerja saya, jika ada?

Misalnya, saya mengembalikan penunjuk cerdas dari fungsi alih-alih penunjuk normal:

std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();

Atau, misalnya, ketika salah satu fungsi saya menerima penunjuk cerdas sebagai parameter alih-alih penunjuk normal:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);
Venemo
sumber
8
Satu-satunya cara untuk mengetahuinya adalah dengan mengukur kode Anda.
Basile Starynkevitch
Mana yang kamu maksud std::unique_ptratau std::shared_ptr?
Stefan
10
Jawabannya adalah 42. (kata lain, siapa tahu, Anda perlu membuat profil kode Anda dan memahami di perangkat keras Anda untuk beban kerja biasa Anda.)
Nim
Aplikasi Anda perlu memanfaatkan petunjuk cerdas secara ekstrem agar menjadi signifikan.
pengguna2672165
Biaya menggunakan shared_ptr dalam fungsi penyetel sederhana sangat buruk dan akan menambah beberapa 100% overhead.
Lothar

Jawaban:

176

std::unique_ptr memiliki overhead memori hanya jika Anda menyediakannya dengan beberapa penghapus non-sepele.

std::shared_ptr selalu memiliki overhead memori untuk penghitung referensi, meskipun sangat kecil.

std::unique_ptr memiliki overhead waktu hanya selama konstruktor (jika harus menyalin penghapus yang disediakan dan / atau menginisialisasi null penunjuk) dan selama destruktor (untuk menghancurkan objek yang dimiliki).

std::shared_ptrmemiliki waktu tambahan di konstruktor (untuk membuat penghitung referensi), di destruktor (untuk mengurangi penghitung referensi dan mungkin menghancurkan objek) dan dalam operator penugasan (untuk menambah penghitung referensi). Karena jaminan keamanan utas std::shared_ptr, kenaikan / penurunan ini bersifat atomik, sehingga menambah overhead lagi.

Perhatikan bahwa tidak ada dari mereka yang memiliki overhead waktu dalam dereferensi (dalam mendapatkan referensi ke objek yang dimiliki), sementara operasi ini tampaknya paling umum untuk pointer.

Singkatnya, ada beberapa overhead, tetapi itu seharusnya tidak membuat kode menjadi lambat kecuali Anda terus membuat dan menghancurkan pointer pintar.

lisyarus
sumber
11
unique_ptrtidak memiliki overhead di destruktor. Ini persis sama seperti yang Anda lakukan dengan pointer mentah.
R. Martinho Fernandes
6
@ R.MartinhoFernandes membandingkan dengan penunjuk mentah itu sendiri, ia memiliki waktu overhead di destruktor, karena destruktor penunjuk mentah tidak melakukan apa pun. Dibandingkan dengan bagaimana pointer mentah mungkin akan digunakan, itu pasti tidak memiliki overhead.
lisyarus
3
Perlu dicatat bahwa bagian dari biaya konstruksi / penghancuran / penugasan shared_ptr adalah karena keamanan utas
Joe
1
Juga, bagaimana dengan konstruktor default std::unique_ptr? Jika Anda membuat std::unique_ptr<int>, internal int*diinisialisasi nullptrapakah Anda suka atau tidak.
Martin Drozdik
1
@MartinDrozdik Dalam kebanyakan situasi Anda akan melakukan null-inisialisasi pointer mentah juga, untuk memeriksa nullity nanti, atau sesuatu seperti itu. Namun demikian, menambahkan ini pada jawaban, terima kasih.
lisyarus
26

Seperti halnya semua performa kode, satu-satunya cara yang benar-benar andal untuk memperoleh informasi penting adalah mengukur dan / atau memeriksa kode mesin.

Bisa dikatakan, penalaran sederhana mengatakan itu

  • Anda dapat mengharapkan beberapa overhead dalam build debug, karena misalnya operator->harus dijalankan sebagai pemanggilan fungsi sehingga Anda dapat melangkah ke dalamnya (hal ini pada gilirannya karena kurangnya dukungan untuk menandai kelas dan fungsi sebagai non-debug).

  • Karena shared_ptrAnda dapat mengharapkan beberapa overhead dalam pembuatan awal, karena itu melibatkan alokasi dinamis dari blok kontrol, dan alokasi dinamis jauh lebih lambat daripada operasi dasar lainnya di C ++ (gunakan make_sharedbila memungkinkan, untuk meminimalkan overhead itu).

  • Juga karena shared_ptrada beberapa overhead minimal dalam mempertahankan jumlah referensi, misalnya saat meneruskan shared_ptrnilai dengan, tetapi tidak ada biaya tambahan untuk unique_ptr.

Dengan mengingat poin pertama di atas, saat Anda mengukur, lakukan itu untuk men-debug dan merilis build.

Komite standardisasi internasional C ++ telah menerbitkan laporan teknis tentang kinerja , tetapi ini pada tahun 2006, sebelumnya unique_ptrdan shared_ptrditambahkan ke perpustakaan standar. Namun, petunjuk cerdas adalah topi lama pada saat itu, jadi laporan itu juga mempertimbangkan itu. Mengutip bagian yang relevan:

“Jika mengakses nilai melalui penunjuk cerdas yang sepele secara signifikan lebih lambat daripada mengaksesnya melalui penunjuk biasa, kompiler tidak menangani abstraksi secara tidak efisien. Di masa lalu, sebagian besar kompiler memiliki penalti abstraksi yang signifikan dan beberapa kompiler saat ini masih melakukannya. Namun, setidaknya dua kompiler telah dilaporkan memiliki penalti abstraksi di bawah 1% dan penalti lainnya sebesar 3%, jadi menghilangkan overhead semacam ini masih dalam keadaan seni ”

Sebagai tebakan yang diinformasikan, "baik dalam keadaan seni" telah dicapai dengan kompiler paling populer saat ini, pada awal 2014.

Cheers and hth. - Alf
sumber
Bisakah Anda menyertakan beberapa detail dalam jawaban Anda tentang kasus yang saya tambahkan ke pertanyaan saya?
Venemo
Ini mungkin benar 10 tahun yang lalu atau lebih, tetapi hari ini, memeriksa kode mesin tidak berguna seperti yang disarankan orang di atas. Bergantung pada bagaimana instruksi dipipel, vektorisasi, ... dan bagaimana kompiler / prosesor menangani spekulasi pada akhirnya adalah seberapa cepatnya. Lebih sedikit kode mesin kode tidak selalu berarti kode lebih cepat. Satu-satunya cara untuk menentukan kinerja adalah dengan membuat profilnya. Ini dapat berubah pada basis prosesor dan juga per kompiler.
Byron
Masalah yang saya lihat adalah, setelah shared_ptrs digunakan di server, maka penggunaan shared_ptrs mulai berkembang biak, dan segera shared_ptrs menjadi teknik manajemen memori default. Jadi sekarang Anda telah mengulangi hukuman abstraksi 1-3% yang diambil berulang kali.
Nathan Doromal
Saya pikir membandingkan build debug adalah pemborosan waktu yang lengkap dan total
Paul Childs
26

Jawaban saya berbeda dari yang lain dan saya benar-benar bertanya-tanya apakah mereka pernah membuat profil kode.

shared_ptr memiliki overhead yang signifikan untuk pembuatan karena alokasi memorinya untuk blok kontrol (yang membuat penghitung ref dan daftar penunjuk ke semua referensi yang lemah). Ia juga memiliki overhead memori yang besar karena ini dan fakta bahwa std :: shared_ptr selalu merupakan tuple 2 pointer (satu ke objek, satu ke blok kontrol).

Jika Anda meneruskan shared_pointer ke suatu fungsi sebagai parameter nilai maka akan setidaknya 10 kali lebih lambat dari panggilan normal dan membuat banyak kode di segmen kode untuk pelepasan tumpukan. Jika Anda melewatkannya dengan referensi, Anda mendapatkan tipuan tambahan yang juga bisa lebih buruk dalam hal kinerja.

Itulah mengapa Anda tidak boleh melakukan ini kecuali fungsinya benar-benar terlibat dalam manajemen kepemilikan. Jika tidak, gunakan "shared_ptr.get ()". Itu tidak dirancang untuk memastikan objek Anda tidak mati selama panggilan fungsi normal.

Jika Anda menjadi gila dan menggunakan shared_ptr pada objek kecil seperti pohon sintaks abstrak dalam kompiler atau pada node kecil dalam struktur grafik lainnya, Anda akan melihat penurunan performa yang besar dan peningkatan memori yang besar. Saya telah melihat sistem parser yang ditulis ulang segera setelah C ++ 14 masuk pasar dan sebelum pemrogram belajar menggunakan petunjuk pintar dengan benar. Penulisan ulang itu jauh lebih lambat dari kode lama.

Ini bukan peluru perak dan petunjuk mentah juga tidak buruk menurut definisi. Pemrogram yang buruk itu buruk dan desain yang buruk itu buruk. Desain dengan hati-hati, desain dengan kepemilikan yang jelas dalam pikiran dan coba gunakan shared_ptr sebagian besar pada batas API subsistem.

Jika Anda ingin mempelajari lebih lanjut, Anda dapat menonton Nicolai M. Josuttis pembicaraan yang baik tentang "Harga Nyata Pointer Bersama di C ++" https://vimeo.com/131189627
Ini membahas jauh ke dalam detail implementasi dan arsitektur CPU untuk hambatan tulis, atomic kunci dll. setelah mendengarkan Anda tidak akan pernah membicarakan tentang fitur ini murah. Jika Anda hanya ingin bukti besarnya lebih lambat, lewati 48 menit pertama dan lihat dia menjalankan kode contoh yang berjalan hingga 180 kali lebih lambat (dikompilasi dengan -O3) saat menggunakan penunjuk bersama di mana-mana.

Lothar
sumber
Terima kasih atas jawaban anda! Platform mana yang Anda buat profil? Dapatkah Anda mendukung klaim Anda dengan beberapa data?
Venemo
Saya tidak punya nomor untuk ditunjukkan, tetapi Anda dapat menemukannya di Nico Josuttis talk vimeo.com/131189627
Lothar
6
Pernah dengar std::make_shared()? Juga, saya menemukan demonstrasi penyalahgunaan terang-terangan menjadi buruk agak membosankan ...
Deduplicator
2
Semua yang dapat dilakukan "make_shared" adalah mengamankan Anda dari satu alokasi tambahan dan memberi Anda lebih banyak lokalitas cache jika blok kontrol dialokasikan di depan objek. Itu tidak bisa membantu sama sekali saat Anda mengoper penunjuk. Ini bukanlah akar masalahnya.
Lothar
14

Dengan kata lain, apakah kode saya akan menjadi lebih lambat jika saya menggunakan petunjuk cerdas, dan jika demikian, seberapa lambat?

Lebih lambat? Kemungkinan besar tidak, kecuali Anda membuat indeks besar menggunakan shared_ptrs dan Anda tidak memiliki cukup memori sampai komputer Anda mulai berkerut, seperti seorang wanita tua yang jatuh ke tanah oleh kekuatan yang tak tertahankan dari jauh.

Apa yang akan membuat kode Anda lebih lambat adalah pencarian yang lambat, pemrosesan loop yang tidak perlu, salinan data yang besar, dan banyak operasi tulis ke disk (seperti ratusan).

Keuntungan dari pointer pintar semuanya terkait dengan manajemen. Tetapi apakah biaya overhead diperlukan? Ini tergantung pada penerapan Anda. Misalkan Anda melakukan iterasi pada larik 3 fase, setiap fase memiliki larik 1024 elemen. Membuat sebuah smart_ptruntuk proses ini mungkin berlebihan, karena setelah iterasi selesai Anda akan tahu bahwa Anda harus menghapusnya. Jadi, Anda bisa mendapatkan memori tambahan karena tidak menggunakan filesmart_ptr ...

Tetapi apakah Anda benar-benar ingin melakukan itu?

Kebocoran memori tunggal dapat membuat produk Anda mengalami titik kegagalan (misalkan program Anda membocorkan 4 megabyte setiap jam, akan memakan waktu berbulan-bulan untuk merusak komputer, namun, komputer akan rusak, Anda tahu itu karena ada kebocoran) .

Seperti mengatakan "perangkat lunak Anda dijamin selama 3 bulan, lalu, hubungi saya untuk diservis".

Jadi pada akhirnya masalahnya adalah ... dapatkah Anda menangani risiko ini? tidak menggunakan pointer mentah untuk menangani pengindeksan Anda lebih dari ratusan objek yang berbeda layak kehilangan kendali atas memori.

Jika jawabannya ya, gunakan pointer mentah.

Jika Anda bahkan tidak ingin mempertimbangkannya, a smart_ptradalah solusi yang baik, layak, dan mengagumkan.

Claudiordgz
sumber
4
ok, tapi valgrind bagus dalam memeriksa kemungkinan kebocoran memori, jadi selama Anda menggunakannya, Anda harus aman ™
graywolf
@Paladin Ya, jika Anda dapat menangani memori Anda, smart_ptrsangat berguna untuk tim besar
Claudiordgz
3
Saya menggunakan unique_ptr, ini menyederhanakan banyak hal, tetapi tidak suka shared_ptr, penghitungan referensi tidak terlalu efisien GC dan juga tidak sempurna
graywolf
1
@Paladin Saya mencoba menggunakan petunjuk mentah jika saya dapat merangkum semuanya. Jika itu adalah sesuatu yang akan saya bagikan di semua tempat seperti argumen maka mungkin saya akan mempertimbangkan smart_ptr. Sebagian besar unique_ptrs saya digunakan dalam implementasi besar, seperti metode utama atau jalankan
Claudiordgz
@Lothar Saya melihat Anda memparafrasekan salah satu hal yang saya katakan dalam jawaban Anda: Thats why you should not do this unless the function is really involved in ownership management... jawaban yang bagus, terima kasih, suara positif
Claudiordgz
0

Hanya untuk sekilas dan hanya untuk []operator, ini ~ 5X lebih lambat dari pointer mentah seperti yang ditunjukkan dalam kode berikut, yang dikompilasi menggunakan gcc -lstdc++ -std=c++14 -O0dan menampilkan hasil ini:

malloc []:     414252610                                                 
unique []  is: 2062494135                                                
uq get []  is: 238801500                                                 
uq.get()[] is: 1505169542
new is:        241049490 

Saya mulai belajar c ++, saya memikirkannya: Anda selalu perlu tahu apa yang Anda lakukan dan meluangkan lebih banyak waktu untuk mengetahui apa yang telah dilakukan orang lain di c ++ Anda.

EDIT

Seperti yang ditunjukkan oleh @Mohan Kumar, saya memberikan detail lebih lanjut. Versi gcc adalah 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), Hasil di atas diperoleh saat -O0digunakan, namun, saat saya menggunakan flag '-O2', saya mendapatkan ini:

malloc []:     223
unique []  is: 105586217
uq get []  is: 71129461
uq.get()[] is: 69246502
new is:        9683

Kemudian bergeser ke clang version 3.9.0, -O0adalah:

malloc []:     409765889
unique []  is: 1351714189
uq get []  is: 256090843
uq.get()[] is: 1026846852
new is:        255421307

-O2 dulu:

malloc []:     150
unique []  is: 124
uq get []  is: 83
uq.get()[] is: 83
new is:        54

Hasil dentang -O2luar biasa.

#include <memory>
#include <iostream>
#include <chrono>
#include <thread>

uint32_t n = 100000000;
void t_m(void){
    auto a  = (char*) malloc(n*sizeof(char));
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

void t_u2(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    auto tmp = a.get();
    for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
    auto a = new char[n];
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

int main(){
    auto start = std::chrono::high_resolution_clock::now();
    t_m();
    auto end1 = std::chrono::high_resolution_clock::now();
    t_u();
    auto end2 = std::chrono::high_resolution_clock::now();
    t_u2();
    auto end3 = std::chrono::high_resolution_clock::now();
    t_u3();
    auto end4 = std::chrono::high_resolution_clock::now();
    t_new();
    auto end5 = std::chrono::high_resolution_clock::now();
    std::cout << "malloc []:     " <<  (end1 - start).count() << std::endl;
    std::cout << "unique []  is: " << (end2 - end1).count() << std::endl;
    std::cout << "uq get []  is: " << (end3 - end2).count() << std::endl;
    std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
    std::cout << "new is:        " << (end5 - end4).count() << std::endl;
}
liqg3
sumber
Saya telah menguji kodenya sekarang, hanya 10% lambat saat menggunakan penunjuk unik.
Mohan Kumar
8
jangan pernah melakukan benchmark dengan -O0atau men-debug kode. Outputnya akan sangat tidak efisien . Selalu gunakan setidaknya -O2(atau -O3saat ini karena beberapa vektorisasi tidak dilakukan di -O2)
phuclv
1
Jika Anda punya waktu dan ingin rehat kopi, ambil -O4 untuk mendapatkan pengoptimalan waktu tautan dan semua fungsi abstraksi kecil menjadi sebaris dan menghilang.
Lothar
Anda harus menyertakan freepanggilan dalam pengujian malloc, dan delete[]untuk baru (atau membuat variabel astatis), karena unique_ptrs memanggildelete[] bawah tenda, di destruktornya.
RnMss