Quanto costa l'overhead dei puntatori intelligenti rispetto ai normali puntatori in C ++?


101

Qual è il sovraccarico dei puntatori intelligenti rispetto ai normali puntatori in C ++ 11? In altre parole, il mio codice sarà più lento se utilizzo i puntatori intelligenti e, in tal caso, quanto più lento?

In particolare, sto chiedendo informazioni su C ++ 11 std::shared_ptre std::unique_ptr.

Ovviamente, le cose spinte verso il basso nello stack saranno più grandi (almeno penso di sì), perché un puntatore intelligente deve anche memorizzare il suo stato interno (conteggio dei riferimenti, ecc.), La domanda è davvero: quanto sta andando a influenzare le mie prestazioni, se non del tutto?

Ad esempio, restituisco un puntatore intelligente da una funzione invece di un normale puntatore:

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

O, ad esempio, quando una delle mie funzioni accetta un puntatore intelligente come parametro invece di un normale puntatore:

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

8
L'unico modo per saperlo è confrontare il tuo codice.
Basile Starynkevitch

A quale ti riferisci? std::unique_ptro std::shared_ptr?
stefan

10
La risposta è 42. (altre parole, chissà, devi profilare il tuo codice e capire sul tuo hardware per il tuo carico di lavoro tipico.)
Nim

La tua applicazione deve fare un uso estremo di puntatori intelligenti perché sia ​​significativa.
user2672165

Il costo dell'utilizzo di shared_ptr in una semplice funzione setter è terribile e aggiungerà un overhead multiplo del 100%.
Lothar

Risposte:


176

std::unique_ptr ha un sovraccarico di memoria solo se gli si fornisce un deleter non banale.

std::shared_ptr ha sempre un sovraccarico di memoria per il contatore di riferimento, sebbene sia molto piccolo.

std::unique_ptr ha un sovraccarico di tempo solo durante il costruttore (se deve copiare il deleter fornito e / o inizializzare il puntatore da null) e durante il distruttore (per distruggere l'oggetto posseduto).

std::shared_ptrha un overhead di tempo nel costruttore (per creare il contatore dei riferimenti), nel distruttore (per decrementare il contatore dei riferimenti ed eventualmente distruggere l'oggetto) e nell'operatore di assegnazione (per incrementare il contatore dei riferimenti). A causa delle garanzie di sicurezza del thread di std::shared_ptr, questi incrementi / decrementi sono atomici, aggiungendo così un po 'più di overhead.

Si noti che nessuno di loro ha un sovraccarico di tempo nel dereferenziare (nell'ottenere il riferimento all'oggetto posseduto), mentre questa operazione sembra essere la più comune per i puntatori.

Per riassumere, c'è un po 'di overhead, ma non dovrebbe rallentare il codice a meno che non crei e distruggi continuamente puntatori intelligenti.


11
unique_ptrnon ha overhead nel distruttore. Funziona esattamente come faresti con un puntatore grezzo.
R. Martinho Fernandes

6
@ R.MartinhoFernandes rispetto al puntatore grezzo stesso, ha un sovraccarico di tempo nel distruttore, poiché il distruttore del puntatore grezzo non fa nulla. Rispetto a come verrebbe probabilmente utilizzato un puntatore grezzo, sicuramente non ha alcun sovraccarico.
lisyarus

3
Vale la pena notare che parte del costo di costruzione / distruzione / assegnazione shared_ptr è dovuto alla sicurezza dei thread
Joe

1
Inoltre, per quanto riguarda il costruttore predefinito di std::unique_ptr? Se costruisci a std::unique_ptr<int>, l'interno int*viene inizializzato a nullptrpiacimento o meno.
Martin Drozdik

1
@MartinDrozdik Nella maggior parte delle situazioni avresti null-inizializzato anche il puntatore non elaborato, per verificare che sia null in seguito, o qualcosa del genere. Tuttavia, aggiunto questo alla risposta, grazie.
lisyarus

26

Come per tutte le prestazioni del codice, l'unico mezzo veramente affidabile per ottenere informazioni concrete è misurare e / o ispezionare il codice macchina.

Detto questo, lo dice un semplice ragionamento

  • Ci si può aspettare un certo sovraccarico nelle build di debug, poiché ad esempio operator->deve essere eseguito come una chiamata di funzione in modo che tu possa entrarci (questo a sua volta è dovuto alla mancanza generale di supporto per contrassegnare classi e funzioni come non debug).

  • Perché ci shared_ptrsi può aspettare un po 'di overhead nella creazione iniziale, poiché ciò implica l'allocazione dinamica di un blocco di controllo e l'allocazione dinamica è molto più lenta di qualsiasi altra operazione di base in C ++ (usatela make_sharedquando praticamente possibile, per minimizzare tale overhead).

  • Anche perché shared_ptrc'è un sovraccarico minimo nel mantenere un conteggio dei riferimenti, ad esempio quando si passa un shared_ptrvalore, ma non c'è un tale sovraccarico per unique_ptr.

Tenendo presente il primo punto sopra, quando misuri, fallo sia per il debug che per le build di rilascio.

Il comitato internazionale di standardizzazione C ++ ha pubblicato un rapporto tecnico sulle prestazioni , ma questo è stato nel 2006, prima unique_ptre shared_ptrsono stati aggiunti alla libreria standard. Tuttavia, i puntatori intelligenti erano ormai obsoleti, quindi il rapporto considerava anche questo. Citando la parte rilevante:

“Se l'accesso a un valore tramite un banale puntatore intelligente è notevolmente più lento rispetto all'accesso tramite un normale puntatore, il compilatore sta gestendo in modo inefficiente l'astrazione. In passato, la maggior parte dei compilatori aveva significative penalità di astrazione e molti compilatori attuali lo fanno ancora. Tuttavia, è stato segnalato che almeno due compilatori hanno penalità di astrazione inferiori all'1% e un altro una penalità del 3%, quindi eliminare questo tipo di overhead rientra nello stato dell'arte "

Secondo un'ipotesi informata, il "bene entro lo stato dell'arte" è stato raggiunto con i compilatori più popolari oggi, all'inizio del 2014.


Potresti includere alcuni dettagli nella tua risposta sui casi che ho aggiunto alla mia domanda?
Venemo

Questo potrebbe essere stato vero 10 o più anni fa, ma oggi l'ispezione del codice macchina non è così utile come suggerisce la persona sopra. A seconda di come le istruzioni sono pipeline, vettorializzate, ... e come il compilatore / processore gestisce la speculazione alla fine è quanto è veloce. Meno codice macchina codice non significa necessariamente codice più veloce. L'unico modo per determinare la performance è profilarla. Questo può cambiare in base al processore e anche al compilatore.
Byron

Un problema che ho riscontrato è che, una volta che shared_ptrs viene utilizzato in un server, l'utilizzo di shared_ptrs inizia a proliferare e presto shared_ptrs diventa la tecnica di gestione della memoria predefinita. Quindi ora hai ripetuto le penalità di astrazione dell'1-3% che vengono applicate più e più volte.
Nathan Doromal

Penso che il benchmarking di una build di debug sia una completa e totale perdita di tempo
Paul Childs il

26

La mia risposta è diversa dalle altre e mi chiedo davvero se abbiano mai profilato il codice.

shared_ptr ha un sovraccarico significativo per la creazione a causa della sua allocazione di memoria per il blocco di controllo (che mantiene il contatore ref e un elenco di puntatori a tutti i riferimenti deboli). Ha anche un enorme sovraccarico di memoria a causa di questo e del fatto che std :: shared_ptr è sempre una tupla di 2 puntatori (uno all'oggetto, uno al blocco di controllo).

Se si passa un shared_pointer a una funzione come parametro di valore, sarà almeno 10 volte più lento di una normale chiamata e creerà molti codici nel segmento di codice per lo svolgimento dello stack. Se lo passi per riferimento, ottieni un ulteriore riferimento indiretto che può essere anche molto peggiore in termini di prestazioni.

Ecco perché non dovresti farlo a meno che la funzione non sia realmente coinvolta nella gestione della proprietà. Altrimenti usa "shared_ptr.get ()". Non è progettato per assicurarsi che l'oggetto non venga ucciso durante una normale chiamata di funzione.

Se impazzisci e usi shared_ptr su piccoli oggetti come un albero di sintassi astratto in un compilatore o su piccoli nodi in qualsiasi altra struttura di grafi, vedrai un enorme calo delle prestazioni e un enorme aumento della memoria. Ho visto un sistema parser che è stato riscritto subito dopo che C ++ 14 è arrivato sul mercato e prima che il programmatore imparasse a usare correttamente i puntatori intelligenti. La riscrittura è stata molto più lenta del vecchio codice.

Non è un proiettile d'argento e nemmeno i puntatori grezzi non sono male per definizione. I cattivi programmatori sono cattivi e il cattivo design è cattivo. Progettare con cura, progettare con una chiara proprietà in mente e provare a utilizzare shared_ptr principalmente sul confine API del sottosistema.

Se vuoi saperne di più puoi guardare Nicolai M. Josuttis parlare di "Il prezzo reale dei puntatori condivisi in C ++" https://vimeo.com/131189627
Approfondisci i dettagli di implementazione e l'architettura della CPU per le barriere di scrittura, atomico serrature ecc. una volta che ascolti non parlerai mai del fatto che questa funzione è economica. Se vuoi solo una prova della grandezza più lenta, salta i primi 48 minuti e guardalo mentre esegue un codice di esempio che viene eseguito fino a 180 volte più lentamente (compilato con -O3) quando si usa il puntatore condiviso ovunque.


Grazie per la tua risposta! Su quale piattaforma hai profilato? Puoi eseguire il backup delle tue affermazioni con alcuni dati?
Venemo

Non ho numero da mostrare, ma puoi trovarne alcuni in Nico Josuttis talk vimeo.com/131189627
Lothar

6
Mai sentito parlare std::make_shared()? Inoltre, trovo che le dimostrazioni di abuso palese siano un po 'noiose ...
Deduplicator

2
Tutto ciò che "make_shared" può fare è salvarti da un'allocazione aggiuntiva e darti un po 'più di località nella cache se il blocco di controllo è allocato davanti all'oggetto. Non può non essere d'aiuto quando passi il puntatore. Questa non è la radice dei problemi.
Lothar

14

In altre parole, il mio codice sarà più lento se utilizzo i puntatori intelligenti e, in tal caso, quanto più lento?

Più lentamente? Molto probabilmente no, a meno che tu non stia creando un enorme indice usando shared_ptrs e non hai abbastanza memoria al punto che il tuo computer inizia a raggrinzirsi, come una vecchia signora che viene precipitata a terra da una forza insopportabile da lontano.

Ciò che renderebbe il tuo codice più lento sono ricerche lente, elaborazione di loop non necessaria, enormi copie di dati e molte operazioni di scrittura su disco (come centinaia).

I vantaggi di un puntatore intelligente sono tutti legati alla gestione. Ma è necessario il sovraccarico? Dipende dalla tua implementazione. Supponiamo che tu stia iterando su un array di 3 fasi, ogni fase ha un array di 1024 elementi. La creazione di un smart_ptrper questo processo potrebbe essere eccessivo, poiché una volta completata l'iterazione saprai che devi cancellarla. Quindi potresti guadagnare memoria extra dal non usare un filesmart_ptr ...

Ma lo vuoi davvero fare?

Una singola perdita di memoria potrebbe far sì che il tuo prodotto abbia un punto di guasto nel tempo (diciamo che il tuo programma perde 4 megabyte ogni ora, ci vorrebbero mesi per rompere un computer, tuttavia, si romperà, lo sai perché la perdita è lì) .

È come dire "il tuo software è garantito per 3 mesi, quindi chiamami per assistenza".

Quindi alla fine è davvero una questione di ... puoi gestire questo rischio? se utilizzare un puntatore grezzo per gestire l'indicizzazione su centinaia di oggetti diversi vale la pena perdere il controllo della memoria.

Se la risposta è sì, usa un puntatore grezzo.

Se non vuoi nemmeno prenderlo in considerazione, a smart_ptrè una soluzione buona, praticabile e fantastica.


4
ok, ma valgrind è bravo a controllare eventuali perdite di memoria, quindi finché lo usi dovresti essere al sicuro ™
graywolf

@Paladin Sì, se riesci a gestire la tua memoria, smart_ptrsono davvero utili per le grandi squadre
Claudiordgz

3
Uso unique_ptr, semplifica molte cose, ma non mi piace shared_ptr, il conteggio dei riferimenti non è molto efficiente GC e neanche perfetto
graywolf

1
@Paladin cerco di usare puntatori grezzi se posso incapsulare tutto. Se è qualcosa che passerò dappertutto come un argomento, forse considererò uno smart_ptr. La maggior parte dei miei unique_ptr vengono utilizzati nella grande implementazione, come un metodo principale o run
Claudiordgz

@ Lothar, vedo che hai parafrasato una delle cose che ho detto nella tua risposta: Thats why you should not do this unless the function is really involved in ownership management... ottima risposta, grazie, voto positivo
Claudiordgz

0

Solo per un assaggio e solo per l' []operatore, è ~ 5 volte più lento del puntatore grezzo come dimostrato nel codice seguente, che è stato compilato utilizzando gcc -lstdc++ -std=c++14 -O0e restituito questo risultato:

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

Sto iniziando a imparare il c ++, ho questo nella mia mente: devi sempre sapere cosa stai facendo e prenderti più tempo per sapere cosa hanno fatto gli altri nel tuo c ++.

MODIFICARE

Come indicato da @Mohan Kumar, ho fornito maggiori dettagli. La versione gcc è 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), Il risultato sopra è stato ottenuto quando -O0viene utilizzato, tuttavia, quando uso il flag '-O2', ho ottenuto questo:

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

Quindi spostato in clang version 3.9.0, -O0era:

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

-O2 era:

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

Il risultato del clang -O2è sorprendente.

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

Ho testato il codice ora, è solo il 10% lento quando si utilizza il puntatore univoco.
Mohan Kumar

8
mai e poi mai -O0eseguire il benchmark con o eseguire il debug del codice. L'output sarà estremamente inefficiente . Usa sempre almeno -O2(o al -O3giorno d'oggi perché alcune vettorializzazioni non vengono eseguite -O2)
phuclv

1
Se hai tempo e vuoi una pausa caffè, prendi -O4 per ottenere l'ottimizzazione del tempo di collegamento e tutte le minuscole funzioni di astrazione diventano in linea e svaniscono.
Lothar

Dovresti includere una freechiamata nel test malloc e delete[]per new (o rendere la variabile astatica), perché le unique_ptrs stanno chiamando delete[]sotto il cofano, nei loro distruttori.
RnMss
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.