In C ++; Quanto dovrebbe essere grande un oggetto [che sarà trasferito tra le funzioni] prima che io consideri di delegarlo all'heap?

8

Nella mia programmazione quotidiana, tendo ad usare pochissimi puntatori, non solo perché voglio mantenere il mio codice semplice e privo di errori, ma perché presumo che la programmazione che faccio non abbia oggetti abbastanza grandi da trarre vantaggio dalla delega all'heap.

Al massimo; il mio oggetto più grande sarebbe probabilmente un QString di forse un milione di caratteri.

Tuttavia, presumo che non sia abbastanza grande da trarre beneficio dall'heap; Come dovrei fare per stimare se un oggetto che ho creato è abbastanza grande da trarre beneficio da essere trasformato in un puntatore?

    
posta Akiva 10.11.2016 - 23:25
fonte

6 risposte

12

Poiché la maggior parte delle implementazioni prende l'heap e lo stack dallo stesso blocco di memoria (in crescita da entrambe le estremità) non ha importanza. La dimensione non è un motivo per preferire l'heap sopra lo stack. La durata del tuo oggetto è. Dovrebbe morire una volta fuori dal campo di applicazione o no? Altrimenti, quando?

    
risposta data 11.11.2016 - 01:16
fonte
8

Un milione di caratteri non deve essere passato per valore, perché il passaggio per valore (oltre un paio di parole che potrebbero contenere registri) viene eseguito tramite lo stack. E lo spazio dello stack è sempre limitato.

Fortunatamente, quando si usa QString, viene passato solo un oggetto di piccole dimensioni in base al valore: l'oggetto stesso utilizza un puntatore a un'area di memoria in cui sono memorizzati i milioni di byte. Passare per valore significa tuttavia che viene eseguita una copia (eccetto se è possibile utilizzare la semantica del movimento) copiando i milioni di byte. Quando non è richiesto il valore, preferisci passare un riferimento const.

Infine puoi considerare di usare i puntatori in una fasion sicura usando i puntatori intelligenti standard come unique_ptr o shared_ptr.

    
risposta data 11.11.2016 - 09:17
fonte
6

Quindi la domanda è fondamentalmente: "quando dovrei usare new ?" Le variabili locali usano sempre lo spazio di stack, ma possiamo allocare esplicitamente gli oggetti nello heap con un'espressione new .

Quando decidi a favore o contro new , non decidiamo in base alla dimensione dell'oggetto, poiché ci sono criteri più importanti:

  • conosciamo staticamente la dimensione dell'oggetto? Raramente è sensato allocare un array sullo stack, in genere gli array sono statici o allocati dinamicamente.

  • Qual è la durata dell'oggetto? La sua durata si esaurisce quando il flusso di controllo lascia l'ambito corrente? In tal caso, è necessario utilizzare una variabile locale per creare l'oggetto. Se dovrebbe essere in grado di vivere più a lungo, abbiamo bisogno di un oggetto assegnato all'heap.

  • Stiamo cercando di scrivere un codice eccezionalmente sicuro? (Sì!) Se è così, gestire la proprietà di più di un puntatore nudo è estremamente noioso e piuttosto incline ai bug. Anche se assegniamo un oggetto con new , dovremmo gestirlo tramite un puntatore intelligente come std::unique_ptr .

La maggior parte degli oggetti è piuttosto piccola. Per esempio. una variabile locale std::vector utilizza probabilmente solo tre parole di memoria dello stack, indipendentemente dalla quantità di dati memorizzati. Internamente, assegna un buffer per i dati sull'heap. Lo stesso sarebbe vero per i tipi di corda. Se crei una classe che è piuttosto grande, ci sono modi per ridurne le dimensioni. Ma molto prima che ciò accada, il numero crescente di campi di istanze dovrebbe ricordarti il principio della responsabilità unica: questa classe sta facendo troppe cose non correlate? Possiamo descrivere la classe in modo più semplice come una collaborazione di più oggetti più piccoli? Ma questa è una domanda di design.

Quando passiamo un oggetto a una funzione come parametro, noi come chiamanti non possiamo decidere come viene passata la variabile. Questo fa parte della firma della funzione. Si noti che possiamo passare oggetti tramite puntatore o riferimento anche quando sono stati allocati nello stack. I possibili tipi di argomenti sono:

  • I riferimenti const di const T& arg sono il solito modo di passare i parametri. Un riferimento è simile a un puntatore, quindi la dimensione del tipo T è irrilevante.

  • Una copia T arg è in genere eseguita solo in uno dei due casi:

    1. La dimensione è molto piccola (fino a un paio di parole grandi) e il tipo è economico da copiare e non ha metodi virtuali. Descrive i tipi numerici incorporati e forse i piccoli POCO definiti dall'utente (semplici vecchi oggetti C, come le strutture).
    2. Dovremmo comunque creare una copia. A volte ciò si verifica nei sovraccarichi dell'operatore come T operator+(T lhs, T const& rhs) { lhs += rhs; return lhs; } .
  • I riferimenti non costali T& arg vengono usati ogni volta che è necessario modificare l'oggetto, o quando il tipo T non è stato scritto pensando alla correttezza.

  • I puntatori const T* arg o T* arg sono raramente visti nel moderno codice C ++. Un riferimento può essere creato solo da un oggetto esistente. Al contrario, i puntatori possono essere nulli o puntare in una memoria non valida. Pertanto, i puntatori non devono essere utilizzati per evitare copie (utilizzare riferimenti) o per rendere possibili le chiamate al metodo virtuale (di nuovo, utilizzare riferimenti). Questo lascia dei puntatori a cose del tipo pointer-y, come: usare il puntatore come array (preferire le collezioni standard), quando si esegue l'aritmetica del puntatore (preferisce gli iteratori), o quando si vuole riassegnare un puntatore a un oggetto diverso, che non è possibile con riferimenti.

risposta data 11.11.2016 - 10:51
fonte
2

At most; my largest object would probably be a QString of maybe a million characters.

sizeof(QString) è 8 byte su sistemi a 64 bit, non importa quanto grande o piccolo sia il suo contenuto, perché i contenuti sono sempre allocati nell'heap, come nel caso di std::vector . QString è effettivamente solo un handle per la memoria sull'heap. Quindi, anche se crei un oggetto QString nello stack e inserisci un milione di caratteri, è comunque sufficiente utilizzare 8 byte di spazio di stack al massimo, se non si vive direttamente in un registro.

Se hai tentato di allocare un milione di caratteri unicode nello stack, in molti casi questo richiederebbe un overflow.

Nella maggior parte del normale tipo di codice C ++ le persone scrivono, in genere non è necessario pensare alla distinzione tra stack e heap, dal momento che è completamente distratto da te con le strutture dati standard che usi. La distinzione è generalmente più se si usa, diciamo, unique_ptr o meno nell'implementazione di una classe. In tal caso, ciò implica l'overhead dell'heap aggiuntivo, un ulteriore livello di riferimento indiretto e una frammentazione della memoria.

Se lavori con oggetti che non gestiscono la propria memoria separatamente nell'heap, come se volessi usare std::array<T, N> nello stack, allora forse un intervallo ragionevole (solo un numero approssimativo e grezzo da utilizzare di) è in genere nell'intervallo tra centinaia di byte e forse un paio di kilobyte per una funzione che non viene chiamata in modo ricorsivo o che chiama altre funzioni che allocano una grande quantità di spazio di stack per essere ragionevolmente sicuri contro gli overflow dello stack sulla maggior parte delle macchine desktop e sistemi operativi almeno. Se si tratta di un array di numeri interi a 32 bit, potrei usare l'heap se ce ne sono più di 128 da archiviare (più di 512 byte), ad esempio, a quel punto potresti invece utilizzare std::vector . Esempio:

void some_func(int n, ...)
{
    // 'std::array' is a purely contiguous structure (not
    // storing pointers to memory allocated elsewhere), so
    // here we are actually allocating sizeof(int) * 128
    // bytes on the stack.
    std::array<int, 128> stack_array;
    int* data = stack_array.data();

    // Only use heap if n is too large to fit in our
    // stack-allocated array of 128 integers.
    std::vector<int> heap_array;
    if (n > stack.size())
    {
        heap_array.resize(n);
        data = heap_array.data();
    }

    // Do stuff with 'data'.
    ...      
}

Per quanto riguarda la semantica del movimento, applicherei una regola simile perché è in realtà abbastanza economico copiare in profondità anche 512 byte se si tratta semplicemente di copiare la memoria dallo stack allo stack o da due regioni di memoria con una località temporale elevata. Anche se hai una grande classe, Foo , con una dozzina di membri di dati in cui sizeof(Foo) è come un enorme 256 byte, non la userei così tanto per allocare il suo contenuto sull'heap se può essere evitato. Normalmente stai perfettamente bene se la decisione di utilizzare l'heap o meno si basa più su cose diverse dalle prestazioni, come evitare gli overflow dello stack e modellare strutture di dati di dimensioni variabili (che implicano l'heap a meno che non siano ottimizzati per casi comuni che coinvolgono piccole dimensioni , come piccole ottimizzazioni di stringhe che evitano l'allocazione heap extra per stringhe piccole ma utilizzano l'allocazione heap extra per quelle grandi), consentendo la proprietà condivisa con shared_ptr , utilizzando un pimpl per ridurre le dipendenze in fase di compilazione o allocando sottotipi per il polimorfismo dove potrebbe essere ingombrante per evitare allocazioni di heap in questi casi.

    
risposta data 17.12.2017 - 21:51
fonte
1

Dipende.

Prima di tutto, la cosa più importante è la semantica. Il ritorno per valore non ha senso per i tipi di riferimento.

Inoltre, con l'avvento della semantica del movimento, passare per valore non implica fare una copia, quindi potrebbe essere efficiente anche per oggetti "grandi". E non dimentichiamoci di RVO.

Detto questo, passare per valore può essere costoso anche con oggetti di piccole dimensioni, soprattutto se hanno allocazioni di heap interne o se sono eseguite in un ciclo chiuso.

Puoi continuare a passare in base al valore fino a quando il profilling non mostra un problema, in pratica. La mia euristica per questo non è preoccuparsi di qualcosa di meno di ~ 100 byte, specialmente se non ha bisogno di allocazione / sincronizzazione dell'heap, a meno che non sia in un ciclo stretto.

    
risposta data 11.11.2016 - 03:59
fonte
1

Per la maggior parte, la risposta dell'arancia candita è corretta. In generale, se vuoi che qualcosa vada via quando esci dalla funzione, mettilo in pila. Tuttavia, c'è una sottigliezza che devi essere a conoscenza. Una pila ha una dimensione. Se hai un'applicazione multi-thread, ci saranno più stack, uno per ogni thread. Questi stack sono spesso contigui. Quello è uno stack che viene immediatamente dopo l'altro nel blocco di memoria assegnato per il tuo processo. Se metti troppe informazioni su uno stack, può traboccare e colpire lo stack successivo, possibilmente corrompendolo, o eventualmente scrivendo su una pagina protetta e causando un arresto anomalo.

Questo è più probabile con oggetti grandi che sono composti da oggetti più piccoli e con funzioni ricorsive che si chiamano e mettono nello stack più copie delle loro variabili locali. Se si dispone di un oggetto particolarmente grande nello stack in una funzione ricorsiva (o in pila in diverse funzioni non ricorsive che si chiamano a vicenda), si possono avere problemi. Quindi è importante essere consapevoli della dimensione degli oggetti che stai mettendo in pila. Con l'aumentare della profondità dello stack e la dimensione degli oggetti con cui lavori nello stack cresce, corri il rischio di problemi.

Ci sono un certo numero di soluzioni. Puoi fare ciò che QString fa e fare in modo che l'oggetto allochi la sua memoria sull'heap in modo tale che mettere uno nello stack usi solo un po 'di spazio nello stack e il resto sia nell'heap. È possibile allocare semplicemente dall'heap e utilizzare puntatori intelligenti sullo stack, come altri hanno menzionato. Oppure puoi cercare oggetti più piccoli che non hanno questo problema. Tutto dipende dal problema che stai risolvendo.

Ma sappi che ci sono casi in cui la dimensione dell'oggetto può determinare dove dovrebbe essere allocata o come dovrebbe essere gestita.

    
risposta data 18.12.2017 - 03:55
fonte

Leggi altre domande sui tag