C ++ 'daki normal işaretçilerle karşılaştırıldığında akıllı işaretçilerin ek yükü ne kadar?


107

C ++ 11'deki normal işaretçilerle karşılaştırıldığında akıllı işaretçilerin ek yükü ne kadar? Başka bir deyişle, akıllı işaretçiler kullanırsam kodum daha yavaş mı olacak ve eğer öyleyse ne kadar yavaş olacak?

Özellikle, C ++ 11 std::shared_ptrve std::unique_ptr.

Açıkçası, yığından aşağı itilen şeyler daha büyük olacak (en azından öyle düşünüyorum), çünkü akıllı bir işaretçinin aynı zamanda iç durumunu da saklaması gerekiyor (referans sayısı, vb.), Asıl soru şu: performansımı etkiler mi?

Örneğin, normal bir işaretçi yerine bir işlevden akıllı bir işaretçi döndürüyorum:

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

Veya, örneğin, işlevlerimden biri normal bir işaretçi yerine parametre olarak bir akıllı işaretçiyi kabul ettiğinde:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);

9
Bilmenin tek yolu, kodunuzu karşılaştırmaktır.
Basile Starynkevitch

Hangisini kastediyorsun? std::unique_ptrveya std::shared_ptr?
stefan

11
Cevap 42. (başka bir kelime, kim bilir, kodunuzun profilini çıkarmanız ve tipik iş yükünüz için donanımınızı anlamanız gerekir.)
Nim

Uygulamanızın önemli olması için akıllı işaretçilerden aşırı derecede faydalanması gerekir.
user2672165

Bir shared_ptr'yi basit bir ayarlayıcı işlevinde kullanmanın maliyeti korkunçtur ve birden çok% 100 ek yük ekler.
Lothar

Yanıtlar:


185

std::unique_ptr yalnızca, önemsiz olmayan bir silici sağlarsanız bellek ek yüküne sahiptir.

std::shared_ptr çok küçük olmasına rağmen her zaman referans sayacı için bellek ek yükü vardır.

std::unique_ptr yalnızca kurucu sırasında (sağlanan siliciyi kopyalamak ve / veya işaretçiyi sıfırlamak zorunda ise) ve yıkıcı sırasında (sahip olunan nesneyi yok etmek için) zaman ek yüküne sahiptir.

std::shared_ptryapıcıda (referans sayacını oluşturmak için), yıkıcıda (referans sayacını azaltmak ve muhtemelen nesneyi yok etmek için) ve atama operatöründe (referans sayacını artırmak için) zaman ek yükü vardır. İplik güvenliği garantileri nedeniyle std::shared_ptr, bu artışlar / azalmalar atomiktir, dolayısıyla biraz daha fazla ek yük ekler.

Bu işlemin işaretçiler için en yaygın olanı gibi görünürken, bunların hiçbirinin başvurudan uzaklaşmada (sahip olunan nesneye başvuruda bulunmada) zaman ek yükü olmadığını unutmayın.

Özetlemek gerekirse, bazı ek yükler vardır, ancak sürekli olarak akıllı işaretçiler oluşturup yok etmediğiniz sürece kodu yavaşlatmamalıdır.


11
unique_ptryıkıcıda hiçbir ek yükü yoktur. Ham bir işaretçiyle yaptığınız gibi tam olarak aynı şeyi yapar.
R. Martinho Fernandes

6
@ R.MartinhoFernandes, ham işaretçinin kendisine kıyasla, ham işaretçi yıkıcı hiçbir şey yapmadığı için yıkıcıda ek bir zamana sahiptir. Ham bir işaretçinin muhtemelen nasıl kullanılacağına kıyasla, kesinlikle ek yükü yoktur.
lisyarus

3
Paylaşılan_tr yapım / imha / atama maliyetinin bir kısmının iş parçacığı güvenliğinden kaynaklandığını belirtmek gerekir
Joe

1
Ayrıca, varsayılan kurucusu ne olacak std::unique_ptr? Bir inşa ederseniz std::unique_ptr<int>, iç int*, beğenip nullptrbeğenmediğinize göre başlatılır .
Martin Drozdik

1
@MartinDrozdik Çoğu durumda ham göstericiyi de sıfırlayarak başlatırsınız, daha sonra boş olup olmadığını kontrol etmek için veya bunun gibi bir şey. Yine de bunu cevaba ekledim, teşekkürler.
lisyarus

29

Cevabım diğerlerinden farklı ve gerçekten kodun profilini çıkarıp çıkarmadıklarını merak ediyorum.

shared_ptr, kontrol bloğu için bellek ayırması nedeniyle (ref sayacını ve tüm zayıf referanslara bir işaretçi listesini tutan) oluşturma için önemli bir ek yüke sahiptir. Ayrıca bu nedenle ve std :: shared_ptr her zaman 2 işaretli bir tuple (biri nesneye, biri kontrol bloğuna) olduğu için büyük bir bellek ek yüküne sahiptir.

Bir paylaşımlı_ işaretleyiciyi bir değer parametresi olarak bir işleve iletirseniz, normal bir çağrıdan en az 10 kat daha yavaş olacaktır ve yığın çözülmesi için kod segmentinde çok sayıda kod yaratacaktır. Referans olarak geçerseniz, performans açısından da oldukça kötü olabilecek ek bir yönlendirme elde edersiniz.

Bu nedenle, işlev gerçekten sahiplik yönetimine dahil olmadıkça bunu yapmamalısınız. Aksi takdirde "shared_ptr.get ()" kullanın. Normal bir işlev çağrısı sırasında nesnenizin öldürülmediğinden emin olmak için tasarlanmamıştır.

Eğer delirirseniz ve bir derleyicideki soyut sözdizimi ağacı gibi küçük nesnelerde veya başka herhangi bir grafik yapısındaki küçük düğümlerde shared_ptr kullanırsanız, büyük bir performans düşüşü ve büyük bir hafıza artışı göreceksiniz. C ++ 14 piyasaya çıktıktan kısa bir süre sonra ve programcı akıllı işaretçileri doğru kullanmayı öğrenmeden önce yeniden yazılmış bir ayrıştırıcı sistemi gördüm. Yeniden yazma, eski koddan çok daha yavaştı.

Bu bir sihirli değnek değildir ve ham işaretçiler de tanım gereği kötü değildir. Kötü programcılar kötüdür ve kötü tasarım kötüdür. Dikkatli tasarlayın, sahipliği net bir şekilde göz önünde bulundurarak tasarlayın ve shared_ptr'yi çoğunlukla alt sistem API sınırında kullanmaya çalışın.

Daha fazlasını öğrenmek istiyorsanız, Nicolai M. Josuttis'in "C ++ 'da Paylaşılan İşaretçilerin Gerçek Fiyatı" hakkındaki güzel konuşmasını izleyebilirsiniz https://vimeo.com/131189627
Yazma engelleri, atomik için uygulama ayrıntılarının ve CPU mimarisinin derinliklerine iner. kilitler vb. bir kez dinledikten sonra bu özelliğin ucuz olduğundan asla bahsetmeyeceksiniz. Daha yavaş bir büyüklük ispatı istiyorsanız, ilk 48 dakikayı atlayın ve her yerde paylaşılan işaretçi kullanırken 180 kata kadar daha yavaş çalışan (-O3 ile derlenmiş) örnek kod çalıştırmasını izleyin.


Cevabınız için teşekkürler! Hangi platformda profil çıkardınız? İddialarınızı bazı verilerle yedekleyebilir misiniz?
Venemo

Gösterecek numaram yok, ancak Nico Josuttis konuşmasında biraz bulabilirsiniz vimeo.com/131189627
Lothar

7
Hiç duydun std::make_shared()mu? Ayrıca, bariz kötüye kullanımın kötü olduğunu gösteren gösterileri biraz sıkıcı buluyorum ...
Deduplicator

2
"Make_shared" in yapabileceği her şey sizi ek bir tahsisattan korur ve kontrol bloğu nesnenin önünde tahsis edilmişse size biraz daha fazla önbellek konumu sağlar. İşaretçiyi etrafından dolaştırdığınızda hiç yardımcı olamaz. Sorunların kaynağı bu değil.
Lothar

26

Tüm kod performanslarında olduğu gibi, zor bilgiler elde etmenin gerçekten güvenilir tek yolu, makine kodunu ölçmek ve / veya incelemektir .

Bununla birlikte, basit mantık şunu söylüyor:

  • Hata ayıklama yapılarında bazı ek yükler bekleyebilirsiniz, çünkü örneğin operator->, ona adım atabilmeniz için bir işlev çağrısı olarak yürütülmesi gerekir (bu, sınıfları ve işlevleri hata ayıklama dışı olarak işaretlemek için genel destek eksikliğinden kaynaklanır).

  • İçin shared_ptrbir kontrol bloğunun dinamik ayırma gerektirir ve dinamik ayırma çok daha yavaş C ++ başka temel işleminden daha olduğundan size (kullanımını yapmak, ilk yaratılış bazı yükü bekleyebilirsiniz make_sharedzaman pratik olarak mümkün olduğu yükü en aza indirmek için).

  • Ayrıca shared_ptr, örneğin bir shared_ptrdeğere göre geçerken bir referans sayımını sürdürmenin bazı minimum ek yükleri vardır, ancak bunun için böyle bir ek yük yoktur unique_ptr.

Yukarıdaki ilk noktayı aklınızda tutarak, ölçüm yaparken bunu hem hata ayıklama hem de sürüm sürümleri için yapın.

Uluslararası C ++ standardizasyon komitesi bir yayınlamıştır performansına teknik rapor öncesinde, ancak bu 2006 yılında oldu unique_ptrve shared_ptrstandart kütüphanesine eklenmiştir. Yine de, akıllı işaretçiler o noktada eski hataydı, bu nedenle rapor bunu da değerlendirdi. İlgili bölümden alıntı yapmak:

"Önemsiz bir akıllı işaretçi aracılığıyla bir değere erişmek, sıradan bir işaretçi aracılığıyla erişmekten önemli ölçüde daha yavaşsa, derleyici soyutlamayı verimsiz bir şekilde ele alıyor demektir. Geçmişte, çoğu derleyicinin önemli soyutlama cezaları vardı ve mevcut birkaç derleyicide hala var. Bununla birlikte, en az iki derleyicinin% 1'in altında soyutlama cezası ve% 3'ün altında bir cezaya sahip olduğu bildirildi, bu nedenle bu tür genel giderlerin ortadan kaldırılması son teknolojiye uygundur "

Bilinçli bir tahmin olarak, 2014'ün başlarından itibaren bugün en popüler derleyicilerle "en son teknolojinin içinde kuyu" elde edildi.


Soruma eklediğim vakalarla ilgili cevabınıza biraz bilgi ekler misiniz?
Venemo

Bu 10 veya daha fazla yıl önce doğru olabilirdi, ancak bugün, makine kodunu incelemek yukarıdaki kişinin önerdiği kadar yararlı değil. Talimatların nasıl ardışık düzene girdiğine, vektörleştirildiğine ve ... derleyicinin / işlemcinin spekülasyonla nasıl başa çıktığına bağlı olarak sonuçta ne kadar hızlıdır. Daha az kodlu makine kodu, daha hızlı kod anlamına gelmez. Performansı belirlemenin tek yolu onun profilini çıkarmaktır. Bu, işlemci temelinde ve ayrıca her derleyici için değişebilir.
Byron

Gördüğüm bir sorun, bir sunucuda shared_ptr'ler kullanıldığında, shared_ptr'lerin kullanımının çoğalmaya başlaması ve çok geçmeden shared_ptr'lerin varsayılan bellek yönetimi tekniği haline gelmesidir. Yani şimdi tekrar tekrar alınan% 1-3 soyutlama cezalarını tekrarladınız.
Nathan Doromal

Bir hata ayıklama yapısını karşılaştırmanın tam ve mutlak bir zaman kaybı olduğunu düşünüyorum
Paul Childs

14

Başka bir deyişle, akıllı işaretçiler kullanırsam kodum daha yavaş mı olacak ve eğer öyleyse ne kadar yavaş olacak?

Yavaş? Büyük olasılıkla, eğer shared_ptrs kullanarak büyük bir dizin oluşturmuyorsanız ve uzaklardan dayanılmaz bir kuvvet tarafından yere düşen yaşlı bir bayan gibi bilgisayarınızın kırışmaya başladığı noktaya kadar yeterli belleğiniz yoksa.

Kodunuzu yavaşlatan şey, yavaş aramalar, gereksiz döngü işlemleri, büyük veri kopyaları ve diske çok sayıda yazma işlemi (yüzlerce gibi).

Akıllı bir işaretçinin avantajlarının tümü yönetimle ilgilidir. Ancak genel gider gerekli mi? Bu, uygulamanıza bağlıdır. Diyelim ki 3 aşamalı bir dizi üzerinde yineliyorsunuz, her aşama 1024 öğe dizisine sahip. smart_ptrBu işlem için bir tane oluşturmak aşırı olabilir, çünkü yineleme tamamlandığında onu silmeniz gerektiğini bileceksiniz. Böylece, bir smart_ptr...

Ama bunu gerçekten yapmak istiyor musun?

Tek bir bellek sızıntısı, ürününüzün zamanında bir arıza noktasına sahip olmasına neden olabilir (diyelim ki programınız her saat 4 megabayt sızdırıyor, bir bilgisayarı kırmak aylar alır, yine de kırılır, biliyorsunuz çünkü sızıntı var) .

"Yazılımınız 3 ay garantilidir, o zaman beni servis için arayın" demek gibidir.

Yani sonunda gerçekten mesele şu ki ... bu riski kaldırabilir misin? Yüzlerce farklı nesne üzerinde indekslemenizi işlemek için ham bir işaretçi kullanmak, belleğin kontrolünü kaybetmeye değer.

Cevap evet ise, ham bir işaretçi kullanın.

Düşünmek bile istemiyorsanız smart_ptr, a iyi, uygulanabilir ve harika bir çözümdür.


4
tamam, ancak valgrind olası bellek sızıntılarını kontrol etmede iyidir , bu yüzden kullandığınız sürece güvende olmalısınız ™
graywolf

@Paladin Evet, hafızanızı idare edebiliyorsanız, smart_ptrbüyük takımlar için gerçekten kullanışlıdır
Claudiordgz

3
Unique_ptr kullanıyorum, birçok şeyi basitleştiriyor, ancak shared_ptr'den hoşlanmıyorum, referans sayma çok verimli değil ve mükemmel değil
graywolf

1
@Paladin Her şeyi özetleyebilirsem ham işaretçiler kullanmaya çalışıyorum. Bu, bir argüman gibi her yerde dolaşacağım bir şeyse, o zaman belki bir smart_ptr düşünebilirim. Unique_ptr'lerimin çoğu, ana veya çalıştırma yöntemi gibi büyük uygulamada kullanılıyor
Claudiordgz

@Lothar, cevabınızda söylediğim şeylerden birini açıkladığını görüyorum: Thats why you should not do this unless the function is really involved in ownership management... harika cevap, teşekkürler, olumlu oy verildi
Claudiordgz

-1

Sadece bir bakış için ve sadece []operatör için, aşağıdaki kodda gösterildiği gibi ham işaretçiden ~ 5 kat daha yavaştır gcc -lstdc++ -std=c++14 -O0ve bu sonuç kullanılarak derlenmiş ve çıktısı alınmıştır:

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

C ++ 'ı öğrenmeye başlıyorum, aklımda şu var: her zaman ne yaptığınızı bilmeniz ve başkalarının c ++' nızda ne yaptığını bilmek için daha fazla zaman ayırmanız gerekir.

DÜZENLE

@Mohan Kumar'ın söylediği gibi, daha fazla ayrıntı verdim. Gcc sürümü, 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1)yukarıdaki sonuç -O0kullanıldığında elde edildi , ancak '-O2' bayrağını kullandığımda şunu aldım:

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

Sonra kaydırılır clang version 3.9.0, -O0oldu:

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

-O2 şuydu:

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

Clang'ın sonucu -O2inanılmaz.

#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;
}


Kodu şimdi test ettim, benzersiz işaretçiyi kullanırken sadece% 10 yavaş.
Mohan Kumar

8
asla -O0kodla kıyaslama veya hata ayıklama. Çıktı son derece verimsiz olacaktır . Her zaman en azından kullanın -O2(veya -O3bugünlerde bazı vektörleştirmeler yapılmadığı için -O2)
phuclv

1
Zamanınız varsa ve bir kahve molası istiyorsanız, bağlantı süresi optimizasyonunu elde etmek için -O4'ü alın ve tüm küçük küçük soyutlama işlevleri satır içi olur ve kaybolur.
Lothar

freeMalloc testine bir çağrı eklemelisiniz ve delete[]yeni için (veya değişken astatik yapmalısınız ), çünkü s'ler kaputun altında, yıkıcılarında unique_ptrçağırıyorlar delete[].
RnMss
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.