Che è più veloce: allocazione dello stack o allocazione dell'heap


503

Questa domanda può sembrare abbastanza elementare, ma questo è un dibattito che ho avuto con un altro sviluppatore con cui lavoro.

Mi stavo occupando di impilare le cose dove potevo, invece di ammassarle. Stava parlando con me e guardandomi alle spalle e commentando che non era necessario perché sono le stesse prestazioni sagge.

Ho sempre avuto l'impressione che la crescita dello stack fosse un tempo costante e le prestazioni dell'allocazione dell'heap dipendevano dall'attuale complessità dell'heap sia per l'allocazione (trovare un foro della dimensione corretta) sia per la de-allocazione (collasso dei fori per ridurre la frammentazione, come molte implementazioni di librerie standard richiedono tempo per farlo durante le eliminazioni se non sbaglio.

Questo mi sembra qualcosa che probabilmente dipenderà molto dal compilatore. Per questo progetto in particolare sto usando un compilatore Metrowerks per l' architettura PPC . La comprensione di questa combinazione sarebbe molto utile, ma in generale, per GCC e MSVC ++, qual è il caso? L'allocazione dell'heap non è così performante come l'allocazione dello stack? Non c'è differenza? Oppure le differenze sono così minuscole che diventa inutile micro-ottimizzazione.


11
So che è piuttosto antico, ma sarebbe bello vedere alcuni frammenti C / C ++ che dimostrano i diversi tipi di allocazione.
Joseph Weissman,

42
Il tuo allevatore di mucche è terribilmente ignorante, ma è più importante che sia pericoloso perché fa affermazioni autorevoli su cose di cui è terribilmente ignorante. Accisa tali persone dalla tua squadra il più rapidamente possibile.
Jim Balter,

5
Si noti che l'heap è in genere molto più grande dello stack. Se vengono allocati grandi quantità di dati, è necessario inserirli nell'heap oppure modificare le dimensioni dello stack dal sistema operativo.
Paul Draper,

1
Tutte le ottimizzazioni sono, a meno che non si abbiano benchmark o argomenti di complessità che dimostrano diversamente, per impostazione predefinita micro-ottimizzazioni inutili.
Björn Lindqvist,

2
Mi chiedo se il tuo collega abbia principalmente esperienza Java o C #. In quelle lingue, quasi tutto è allocato sotto il cofano, il che potrebbe portare a tali ipotesi.
Cort Ammon,

Risposte:


493

L'allocazione dello stack è molto più veloce poiché tutto ciò che fa è spostare il puntatore dello stack. Utilizzando i pool di memoria, è possibile ottenere prestazioni comparabili dall'allocazione dell'heap, ma ciò comporta una leggera complessità aggiuntiva e il suo mal di testa.

Inoltre, stack vs. heap non è solo una considerazione delle prestazioni; ti dice anche molto sulla durata prevista degli oggetti.


211
E ancora più importante, lo stack è sempre caldo, la memoria che ottieni è molto più probabile che sia nella cache rispetto a qualsiasi memoria allocata dell'heap lontano
Benoît

47
Su alcune architetture (per lo più integrate, che conosco), lo stack può essere archiviato in una memoria on-die veloce (ad esempio SRAM). Questo può fare una differenza enorme!
magro il

38
Perché lo stack è in realtà, uno stack. Non è possibile liberare un pezzo di memoria utilizzato dallo stack a meno che non sia sopra di esso. Non c'è gestione, spingi o fai pop su di essa. D'altra parte, la memoria heap è gestita: chiede al kernel i blocchi di memoria, forse li divide, li unisce, li riutilizza e li libera. Lo stack è davvero pensato per allocazioni veloci e brevi.
Benoît,

24
@Pacerier Perché lo Stack è molto più piccolo dell'Heap. Se si desidera allocare grandi array, è meglio allocarli sull'heap. Se si tenta di allocare un array di grandi dimensioni nello Stack, si otterrebbe uno Stack Overflow. Prova ad esempio in C ++ questo: int t [100000000]; Prova ad esempio t [10000000] = 10; e poi cout << t [10000000]; Dovrebbe darti un overflow dello stack o semplicemente non funzionerà e non ti mostrerà nulla. Ma se si alloca l'array sull'heap: int * t = new int [100000000]; e dopo eseguirà le stesse operazioni, funzionerà perché l'heap ha le dimensioni necessarie per un array così grande.
Lilian A. Moraru,

7
@Pacerier La ragione più ovvia è che gli oggetti nello stack escono dal campo di applicazione all'uscita dal blocco in cui sono allocati.
Jim Balter

166

Lo stack è molto più veloce. Utilizza letteralmente una sola istruzione sulla maggior parte delle architetture, nella maggior parte dei casi, ad esempio su x86:

sub esp, 0x10

(Ciò sposta il puntatore dello stack in basso di 0x10 byte e quindi "alloca" quei byte per l'uso da parte di una variabile.)

Ovviamente, le dimensioni dello stack sono molto, molto limitate, poiché scoprirai rapidamente se utilizzi eccessivamente l'allocazione dello stack o provi a fare la ricorsione :-)

Inoltre, ci sono pochi motivi per ottimizzare le prestazioni del codice che non ne sono effettivamente verificabili, come dimostrato dalla profilazione. L '"ottimizzazione prematura" spesso causa più problemi di quanti ne valga la pena.

La mia regola empirica: se so che avrò bisogno di alcuni dati in fase di compilazione ed è di dimensioni inferiori a qualche centinaio di byte, li impallo. Altrimenti lo ammetto.


20
Un'istruzione, che di solito è condivisa da TUTTI gli oggetti nello stack.
MSalters,

9
Ha sottolineato bene il punto, in particolare il punto di averne effettivamente bisogno. Sono continuamente stupito di come le preoccupazioni della gente riguardo alle prestazioni siano fuori luogo.
Mike Dunlavey,

6
"Deallocation" è anche molto semplice ed è fatto con una singola leaveistruzione.
doc

15
Tieni presente il costo "nascosto" qui, soprattutto per la prima volta che estendi lo stack. Ciò potrebbe causare un errore di pagina, un cambio di contesto nel kernel che deve fare un po 'di lavoro per allocare la memoria (o caricarla dallo scambio, nel peggiore dei casi).
nn.

2
In alcuni casi, puoi persino allocarlo con 0 istruzioni. Se sono note alcune informazioni su quanti byte devono essere allocati, il compilatore può allocarli in anticipo allo stesso tempo allocare altre variabili di stack. In questi casi, non paghi nulla!
Cort Ammon,

119

Onestamente, è banale scrivere un programma per confrontare le prestazioni:

#include <ctime>
#include <iostream>

namespace {
    class empty { }; // even empty classes take up 1 byte of space, minimum
}

int main()
{
    std::clock_t start = std::clock();
    for (int i = 0; i < 100000; ++i)
        empty e;
    std::clock_t duration = std::clock() - start;
    std::cout << "stack allocation took " << duration << " clock ticks\n";
    start = std::clock();
    for (int i = 0; i < 100000; ++i) {
        empty* e = new empty;
        delete e;
    };
    duration = std::clock() - start;
    std::cout << "heap allocation took " << duration << " clock ticks\n";
}

Si dice che una consistenza insensata sia il folletto delle piccole menti . Compensatori apparentemente ottimizzati sono gli hobgoblin delle menti di molti programmatori. Questa discussione era in fondo alla risposta, ma a quanto pare le persone non possono preoccuparsi di leggere così lontano, quindi la sto spostando qui per evitare di ricevere domande a cui ho già risposto.

Un compilatore di ottimizzazione potrebbe notare che questo codice non fa nulla e può ottimizzarlo completamente. È compito dell'ottimizzatore fare cose del genere e combattere l'ottimizzatore è una commissione da pazzi.

Consiglierei di compilare questo codice con l'ottimizzazione disattivata perché non c'è un buon modo per ingannare ogni ottimizzatore attualmente in uso o che sarà in uso in futuro.

Chiunque accenda l'ottimizzatore e poi si lamenta di combatterlo dovrebbe essere soggetto a ridicolo pubblico.

Se mi importasse della precisione dei nanosecondi, non userei std::clock() . Se volessi pubblicare i risultati come tesi di dottorato, farei un affare più grande a riguardo e probabilmente confronterei GCC, Tendra / Ten15, LLVM, Watcom, Borland, Visual C ++, Digital Mars, ICC e altri compilatori. Allo stato attuale, l'allocazione dell'heap impiega centinaia di volte in più rispetto all'allocazione dello stack e non vedo più nulla di utile sull'indagine della domanda.

L'ottimizzatore ha la missione di sbarazzarsi del codice che sto testando. Non vedo alcun motivo per dire all'ottimizzatore di eseguire e quindi provare a ingannare l'ottimizzatore per non effettivamente ottimizzare. Ma se vedessi valore nel farlo, farei una o più delle seguenti operazioni:

  1. Aggiungere un membro di dati a empty, e accedere a quel membro di dati nel ciclo; ma se ho mai letto dal membro dei dati l'ottimizzatore può eseguire una piegatura costante e rimuovere il ciclo; se scrivo sempre e solo al membro dati, l'ottimizzatore potrebbe saltare tutto tranne l'ultima iterazione del ciclo. Inoltre, la domanda non era "allocazione dello stack e accesso ai dati vs. allocazione dell'heap e accesso ai dati".

  2. Dichiarare e volatile, ma volatilespesso compilato in modo errato (PDF).

  3. Prendi l'indirizzo eall'interno del ciclo (e magari assegnalo a una variabile dichiarata externe definita in un altro file). Ma anche in questo caso, il compilatore può notare che - almeno nello stack - everrà sempre allocato allo stesso indirizzo di memoria, e quindi eseguirà una piegatura costante come in (1) sopra. Ottengo tutte le iterazioni del ciclo, ma l'oggetto non viene mai effettivamente assegnato.

Al di là dell'ovvio, questo test è imperfetto in quanto misura sia l'allocazione che la deallocazione e la domanda originale non si poneva sulla deallocazione. Naturalmente le variabili allocate nello stack vengono automaticamente deallocate alla fine del loro ambito, quindi non chiamare deletesignificherebbe (1) distorcere i numeri (la deallocazione dello stack è inclusa nei numeri sull'allocazione dello stack, quindi è giusto misurare la deallocazione dell'heap) e ( 2) causare una perdita di memoria piuttosto scadente, a meno che non conserviamo un riferimento al nuovo puntatore e chiamiamo deletedopo aver effettuato la misurazione del tempo.

Sulla mia macchina, usando g ++ 3.4.4 su Windows, ottengo "0 tick di clock" sia per allocazione di stack che heap per qualcosa di meno di 100000 allocazioni, e anche allora ottengo "0 tick di clock" per allocazione di stack e "15 tick di clock "per l'allocazione dell'heap. Quando misuro 10.000.000 di allocazioni, l'allocazione di stack richiede 31 tick di clock e l'allocazione di heap richiede 1562 tick di clock.


Sì, un compilatore di ottimizzazione può evitare la creazione di oggetti vuoti. Se ho capito bene, potrebbe persino eludere l'intero primo ciclo. Quando ho incrementato le iterazioni a 10.000.000 di allocazioni di stack hanno preso 31 tick di clock e l'allocazione di heap ha preso 1562 tick di clock. Penso che sia sicuro dire che senza dire a g ++ di ottimizzare l'eseguibile, g ++ non ha eluso i costruttori.


Negli anni da quando ho scritto questo, la preferenza su Stack Overflow è stata quella di pubblicare prestazioni da build ottimizzate. In generale, penso che sia corretto. Tuttavia, penso ancora che sia sciocco chiedere al compilatore di ottimizzare il codice quando in realtà non si desidera ottimizzare quel codice. Mi sembra molto simile a pagare un extra per il parcheggio custodito, ma rifiutando di consegnare le chiavi. In questo caso particolare, non voglio che l'ottimizzatore funzioni.

Utilizzando una versione leggermente modificata del benchmark (per indirizzare il punto valido in cui il programma originale non ha allocato qualcosa nello stack ogni volta attraverso il ciclo) e compilare senza ottimizzazioni ma collegandosi alle librerie di rilascio (per indirizzare il punto valido che doniamo non voglio includere alcun rallentamento causato dal collegamento alle librerie di debug):

#include <cstdio>
#include <chrono>

namespace {
    void on_stack()
    {
        int i;
    }

    void on_heap()
    {
        int* i = new int;
        delete i;
    }
}

int main()
{
    auto begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_stack();
    auto end = std::chrono::system_clock::now();

    std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());

    begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_heap();
    end = std::chrono::system_clock::now();

    std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
    return 0;
}

display:

on_stack took 2.070003 seconds
on_heap took 57.980081 seconds

sul mio sistema quando compilato con la riga di comando cl foo.cc /Od /MT /EHsc.

Potresti non essere d'accordo con il mio approccio per ottenere una build non ottimizzata. Va bene: sentiti libero di modificare il benchmark quanto vuoi. Quando attivo l'ottimizzazione, ottengo:

on_stack took 0.000000 seconds
on_heap took 51.608723 seconds

Non perché l'allocazione dello stack sia in realtà istantanea, ma perché qualsiasi compilatore decente può notare che on_stacknon fa nulla di utile e può essere ottimizzato. GCC sul mio laptop Linux nota anche che on_heapnon fa nulla di utile e lo ottimizza anche via:

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds

2
Inoltre, dovresti aggiungere un loop di "calibrazione" all'inizio della tua funzione principale, qualcosa per darti un'idea di quanto tempo stai ricevendo per ogni ciclo di loop e regolare gli altri loop in modo da garantire che il tuo esempio funzioni per un po 'di tempo, invece della costante fissa che stai usando.
Joe Pineda,

2
Sono anche contento che aumentare il numero di volte in cui ogni ciclo di opzioni viene eseguito (oltre a indicare a g ++ di non ottimizzare?) Abbia prodotto risultati significativi. Quindi ora abbiamo fatti concreti per dire che lo stack è più veloce. Grazie per il tuo impegno!
Joe Pineda,

7
È compito dell'ottimizzatore sbarazzarsi di codice come questo. C'è un buon motivo per attivare l'ottimizzatore e quindi impedirne l'ottimizzazione effettiva? Ho modificato la risposta per rendere le cose ancora più chiare: se ti piace combattere l'ottimizzatore, preparati a scoprire quanto sono intelligenti gli autori di compilatori.
Max Lybbert

3
Sono in ritardo, ma vale anche la pena menzionare qui che l'allocazione dell'heap richiede memoria attraverso il kernel, quindi il successo delle prestazioni dipende anche fortemente dall'efficienza del kernel. L'uso di questo codice con Linux (Linux 3.10.7-gentoo # 2 SMP mer 4 set 18:58:21 MDT 2013 x86_64), la modifica per il timer delle risorse umane e l'utilizzo di 100 milioni di iterazioni in ciascun ciclo producono queste prestazioni: stack allocation took 0.15354 seconds, heap allocation took 0.834044 secondscon -O0set, rendendo L'allocazione dell'heap di Linux è più lenta solo di un fattore di circa 5,5 sulla mia macchina particolare.
Taywee,

4
Su Windows senza ottimizzazioni (build di debug) utilizzerà l'heap di debug che è molto più lento dell'heap non di debug. Non credo sia una cattiva idea "ingannare" l'ottimizzatore. Gli autori di compilatori sono intelligenti, ma i compilatori non sono di tipo AI.
paulm,

30

Una cosa interessante che ho imparato sull'allocazione Stack vs.Heap sul processore Xenon Xbox 360, che può applicarsi anche ad altri sistemi multicore, è che l'allocazione su Heap provoca l'inserimento di una Sezione critica per arrestare tutti gli altri core in modo che l'allocazione non è in conflitto. Pertanto, in un circuito ristretto, Stack Allocation era la strada da percorrere per array di dimensioni fisse poiché impediva le stalle.

Questo potrebbe essere un altro accorgimento da considerare se stai codificando multicore / multiproc, in quanto l'allocazione dello stack sarà visualizzabile solo dal core che esegue la tua funzione con ambito, e ciò non influirà su nessun altro core / CPU.


4
Questo è vero per la maggior parte delle macchine multicore, non solo per lo Xenon. Anche Cell deve farlo perché potresti eseguire due thread hardware su quel core PPU.
Crashworks

15
Questo è un effetto dell'implementazione (particolarmente scarsa) dell'allocatore di heap. I migliori allocatori di heap non devono acquisire un blocco su ogni allocazione.
Chris Dodd,

19

È possibile scrivere un allocatore di heap speciale per dimensioni specifiche di oggetti molto performanti. Tuttavia, il generale allocatore di heap non è particolarmente performante.

Sono anche d'accordo con Torbjörn Gyllebring sulla durata prevista degli oggetti. Buon punto!


1
A volte viene definita allocazione di lastre.
Benoit,

8

Non penso che l'allocazione dello stack e l'allocazione dell'heap siano generalmente intercambiabili. Spero anche che le prestazioni di entrambi siano sufficienti per un uso generale.

Consiglio vivamente per articoli di piccole dimensioni, a seconda di quale sia più adatto allo scopo dell'allocazione. Per oggetti di grandi dimensioni, è probabilmente necessario l'heap.

Sui sistemi operativi a 32 bit con più thread, lo stack è spesso piuttosto limitato (anche se in genere ad almeno alcuni mb), perché lo spazio degli indirizzi deve essere scavato e prima o poi uno stack di thread verrà eseguito in un altro. Sui sistemi a thread singolo (Linux glibc single threading comunque) la limitazione è molto inferiore perché lo stack può semplicemente crescere e crescere.

Sui sistemi operativi a 64 bit c'è abbastanza spazio di indirizzamento per rendere gli stack di thread abbastanza grandi.


6

Di solito l'allocazione dello stack consiste semplicemente nella sottrazione dal registro del puntatore dello stack. Questo è molto più veloce della ricerca di un heap.

A volte l'allocazione dello stack richiede l'aggiunta di una o più pagine di memoria virtuale. L'aggiunta di una nuova pagina di memoria azzerata non richiede la lettura di una pagina dal disco, quindi di solito questa sarà ancora molto più veloce della ricerca di un heap (specialmente se anche una parte dell'heap è stata pagata). In una situazione rara, e potresti costruire un esempio del genere, è sufficiente che vi sia spazio sufficiente in una parte dell'heap che è già nella RAM, ma l'allocazione di una nuova pagina per lo stack deve attendere che venga scritta un'altra pagina su disco. In quella rara situazione, l'heap è più veloce.


Non penso che l'heap sia "cercato" a meno che non sia impaginato. La memoria a stato solido abbastanza sicura utilizza un multiplexor e può accedere direttamente alla memoria, quindi alla memoria ad accesso casuale.
Joe Phillips,

4
Ecco un esempio Il programma chiamante chiede di allocare 37 byte. La funzione di libreria cerca un blocco di almeno 40 byte. Il primo blocco nell'elenco gratuito ha 16 byte. Il secondo blocco nell'elenco gratuito ha 12 byte. Il terzo blocco ha 44 byte. La libreria smette di cercare in quel punto.
Programmatore Windows

6

A parte il vantaggio in termini di prestazioni dell'ordine di grandezza rispetto all'allocazione dell'heap, l'allocazione dello stack è preferibile per le applicazioni server di lunga durata. Anche i cumuli meglio gestiti alla fine diventano così frammentati che le prestazioni dell'applicazione peggiorano.


4

Uno stack ha una capacità limitata, mentre un heap non lo è. Lo stack tipico per un processo o thread è di circa 8 KB. Non è possibile modificare la dimensione una volta allocata.

Una variabile stack segue le regole di scoping, mentre una heap no. Se il puntatore dell'istruzione va oltre una funzione, tutte le nuove variabili associate alla funzione scompaiono.

Soprattutto, non è possibile prevedere in anticipo la catena di chiamate della funzione generale. Quindi un'allocazione di soli 200 byte da parte tua può sollevare un overflow dello stack. Questo è particolarmente importante se stai scrivendo una libreria, non un'applicazione.


1
La quantità di spazio di indirizzi virtuali allocata per uno stack in modalità utente su un sistema operativo moderno è probabilmente di almeno 64 kB o superiore per impostazione predefinita (1 MB su Windows). Stai parlando delle dimensioni dello stack del kernel?
bk1e,

1
Sulla mia macchina, la dimensione dello stack predefinita per un processo è 8 MB, non kB. Quanti anni ha il tuo computer?
Greg Rogers,

3

Penso che la vita sia cruciale e se la cosa assegnata debba essere costruita in modo complesso. Ad esempio, nella modellazione basata sulle transazioni, in genere è necessario compilare e passare una struttura di transazione con un gruppo di campi alle funzioni operative. Guarda lo standard OSCI SystemC TLM-2.0 per un esempio.

Allocare questi sullo stack vicino alla chiamata all'operazione tende a causare enormi spese generali, poiché la costruzione è costosa. Il modo migliore è quello di allocare sull'heap e riutilizzare gli oggetti di transazione mediante pool o una semplice politica come "questo modulo ha bisogno di un solo oggetto di transazione".

Questo è molte volte più veloce dell'allocazione dell'oggetto su ogni chiamata di operazione.

Il motivo è semplicemente che l'oggetto ha una costruzione costosa e una vita utile abbastanza lunga.

Direi: prova entrambi e vedi cosa funziona meglio nel tuo caso, perché può davvero dipendere dal comportamento del tuo codice.


3

Probabilmente il problema più grande dell'allocazione dell'heap rispetto all'allocazione dello stack è che l'allocazione dell'heap nel caso generale è un'operazione illimitata e quindi non è possibile utilizzarla laddove i tempi sono un problema.

Per altre applicazioni in cui il tempismo non è un problema, potrebbe non essere così importante, ma se si accumula molto, ciò influirà sulla velocità di esecuzione. Cerca sempre di utilizzare lo stack per memoria di breve durata e spesso allocata (ad esempio nei loop) e il più a lungo possibile - esegui l'allocazione dell'heap all'avvio dell'applicazione.


3

Non è l'allocazione dello stack jsut che è più veloce. Inoltre, vinci molto utilizzando variabili dello stack. Hanno una migliore località di riferimento. E infine, anche la deallocazione è molto più economica.


3

L'allocazione di stack è un paio di istruzioni mentre l'allocatore di heap rtos più veloce che conosco (TLSF) utilizza in media nell'ordine di 150 istruzioni. Inoltre, le allocazioni dello stack non richiedono un blocco perché utilizzano l'archiviazione locale dei thread, che rappresenta un'altra enorme vittoria in termini di prestazioni. Quindi le allocazioni dello stack possono essere 2-3 ordini di grandezza più veloci a seconda di quanto sia pesantemente il tuo ambiente multithread.

In generale, l'allocazione dell'heap è l'ultima risorsa se ti preoccupi delle prestazioni. Un'opzione intermedia praticabile può essere un allocatore di pool fisso che è anche solo un paio di istruzioni e ha un sovraccarico per allocazione molto piccolo, quindi è ottimo per oggetti di piccole dimensioni fisse. Il rovescio della medaglia funziona solo con oggetti di dimensioni fisse, non è intrinsecamente sicuro per i thread e presenta problemi di frammentazione.


3

Preoccupazioni specifiche per il linguaggio C ++

Prima di tutto, non esiste un'allocazione cosiddetta "stack" o "heap" obbligatoria per C ++ . Se si parla di oggetti automatici in ambiti di blocco, non vengono nemmeno "allocati". (A proposito, la durata della memorizzazione automatica in C NON è sicuramente la stessa di "allocata"; quest'ultima è "dinamica" nel linguaggio C ++.) La memoria allocata dinamicamente si trova nell'archivio libero , non necessariamente su "l'heap", sebbene il quest'ultima è spesso l' implementazione (predefinita) .

Sebbene secondo le regole semantiche della macchina astratta , gli oggetti automatici occupino ancora memoria, un'implementazione C ++ conforme può ignorare questo fatto quando può dimostrare che ciò non ha importanza (quando non cambia il comportamento osservabile del programma). Questa autorizzazione è concessa dalla regola as-if in ISO C ++, che è anche la clausola generale che consente le consuete ottimizzazioni (e c'è anche una regola quasi identica in ISO C). Oltre alla regola as-if, ISO C ++ deve anche regole di elisione della copiaconsentire l'omissione di creazioni specifiche di oggetti. Le chiamate del costruttore e del distruttore coinvolte vengono quindi omesse. Di conseguenza, gli oggetti automatici (se presenti) in questi costruttori e distruttori vengono anche eliminati, rispetto alla semantica astratta ingenua implicita dal codice sorgente.

D'altra parte, l'allocazione gratuita del negozio è sicuramente "allocazione" in base alla progettazione. In base alle regole ISO C ++, tale allocazione può essere ottenuta mediante una chiamata di una funzione di allocazione . Tuttavia, dal momento che ISO C ++ 14, esiste una nuova regola (non-come-se) per consentire la fusione di ::operator newchiamate di funzione di allocazione globale (cioè ) in casi specifici. Quindi parti di operazioni di allocazione dinamica possono anche essere non operative come nel caso degli oggetti automatici.

Le funzioni di allocazione allocano risorse di memoria. Gli oggetti possono essere ulteriormente allocati in base all'allocazione utilizzando gli allocatori. Per gli oggetti automatici, vengono presentati direttamente - sebbene sia possibile accedere alla memoria sottostante e utilizzarli per fornire memoria ad altri oggetti (per posizionamento new), ma ciò non ha molto senso come archivio gratuito, perché non c'è modo di spostare risorse altrove.

Tutte le altre preoccupazioni non rientrano nell'ambito del C ++. Tuttavia, possono essere ancora significativi.

Informazioni sulle implementazioni di C ++

Il C ++ non espone record di attivazione reificati o alcuni tipi di continuazioni di prima classe (ad es. Dal famoso call/cc), non c'è modo di manipolare direttamente i frame di record di attivazione - in cui l'implementazione deve posizionare gli oggetti automatici. Una volta che non ci sono interoperazioni (non portatili) con l'implementazione sottostante (codice "nativo" non portatile, come il codice assembly inline), un'omissione dell'allocazione sottostante dei frame può essere abbastanza banale. Ad esempio, quando la funzione chiamata è inline, i frame possono essere effettivamente uniti in altri, quindi non c'è modo di mostrare qual è la "allocazione".

Tuttavia, una volta rispettati gli interops, le cose diventano complesse. Un'implementazione tipica di C ++ esporrà la capacità di interoperabilità su ISA (architettura dell'insieme di istruzioni) con alcune convenzioni di chiamata come limite binario condiviso con il codice nativo (macchina a livello ISA). Ciò sarebbe esplicitamente costoso, in particolare, quando si mantiene il puntatore dello stack , che è spesso direttamente gestito da un registro a livello ISA (con probabilmente istruzioni specifiche per l'accesso alla macchina). Il puntatore dello stack indica il limite del frame superiore della chiamata di funzione (attualmente attiva). Quando viene immessa una chiamata di funzione, è necessario un nuovo frame e il puntatore dello stack viene aggiunto o sottratto (a seconda della convenzione di ISA) da un valore non inferiore alla dimensione del frame richiesta. Il frame viene quindi detto allocatoquando il puntatore dello stack dopo le operazioni. I parametri delle funzioni possono essere passati anche al frame dello stack, a seconda della convenzione di chiamata utilizzata per la chiamata. Il frame può contenere la memoria di oggetti automatici (probabilmente inclusi i parametri) specificati dal codice sorgente C ++. Nel senso di tali implementazioni, questi oggetti sono "assegnati". Quando il controllo esce dalla chiamata di funzione, il frame non è più necessario, di solito viene rilasciato ripristinando il puntatore dello stack allo stato precedente alla chiamata (salvato in precedenza in base alla convenzione di chiamata). Questo può essere visto come "deallocazione". Queste operazioni rendono il record di attivazione efficacemente una struttura di dati LIFO, quindi viene spesso chiamato " stack (chiamata) ".

Poiché la maggior parte delle implementazioni C ++ (in particolare quelle che prendono di mira il codice nativo a livello ISA e utilizzano il linguaggio assembly come output immediato) utilizzano strategie simili come questa, uno schema di "allocazione" così confuso è popolare. Tali allocazioni (così come le deallocazioni) impiegano cicli di macchina, e può essere costoso quando le chiamate (non ottimizzate) si verificano frequentemente, anche se le moderne microarchitettura della CPU possono avere ottimizzazioni complesse implementate dall'hardware per il modello di codice comune (come l'utilizzo di un motore di stack in implementazione PUSH/ POPistruzioni).

Tuttavia, in generale, è vero che il costo dell'allocazione dei frame dello stack è significativamente inferiore a una chiamata a una funzione di allocazione che gestisce il negozio gratuito (a meno che non sia totalmente ottimizzata via) , che può avere centinaia di (se non milioni di :-) operazioni per mantenere il puntatore dello stack e altri stati. Le funzioni di allocazione si basano in genere sull'API fornita dall'ambiente ospitato (ad es. Runtime fornito dal sistema operativo). Diversamente dallo scopo di contenere oggetti automatici per le chiamate di funzioni, tali allocazioni hanno uno scopo generale, quindi non avranno una struttura a trama come una pila. Tradizionalmente, allocare spazio dallo storage del pool chiamato heap (o diversi heap). Diversamente dallo "stack", il concetto "heap" qui non indica la struttura dei dati utilizzata;deriva dalle prime implementazioni linguistiche decenni fa. (A proposito, lo stack di chiamate viene solitamente allocato dall'heap con dimensioni fisse o specificate dall'utente dall'ambiente all'avvio del programma o del thread.) La natura dei casi d'uso rende le allocazioni e le deallocazioni da un heap molto più complicate (rispetto a push o pop di stack frame) e difficilmente possibile essere ottimizzati direttamente dall'hardware.

Effetti sull'accesso alla memoria

La consueta allocazione dello stack mette sempre il nuovo frame in cima, quindi ha una località abbastanza buona. Questo è amichevole per la cache. OTOH, la memoria allocata casualmente nel negozio gratuito non ha tale proprietà. Da ISO C ++ 17, ci sono modelli di risorse del pool forniti da <memory>. Lo scopo diretto di tale interfaccia è quello di consentire che i risultati delle allocazioni consecutive siano ravvicinati nella memoria. Ciò riconosce il fatto che questa strategia è generalmente buona per le prestazioni con implementazioni contemporanee, ad esempio essere amichevole da memorizzare nelle architetture moderne. Tuttavia, si tratta delle prestazioni dell'accesso piuttosto che dell'allocazione .

Concorrenza

Le aspettative di accesso simultaneo alla memoria possono avere effetti diversi tra stack e heap. Uno stack di chiamate è in genere di proprietà esclusiva di un thread di esecuzione in un'implementazione C ++. OTOH, i cumuli sono spesso condivisi tra i thread in un processo. Per tali cumuli, le funzioni di allocazione e deallocazione devono proteggere la struttura di dati amministrativi interni condivisi dalla corsa dei dati. Di conseguenza, le allocazioni di heap e le deallocazioni potrebbero avere un sovraccarico aggiuntivo a causa delle operazioni di sincronizzazione interna.

Efficienza nello spazio

A causa della natura dei casi d'uso e delle strutture di dati interne, gli heap possono soffrire di frammentazione della memoria interna , mentre lo stack no. Ciò non ha un impatto diretto sulle prestazioni di allocazione della memoria, ma in un sistema con memoria virtuale , la scarsa efficienza dello spazio può degenerare le prestazioni complessive dell'accesso alla memoria. Ciò è particolarmente terribile quando l'HDD viene utilizzato come scambio di memoria fisica. Può causare una latenza piuttosto lunga - a volte miliardi di cicli.

Limitazioni delle allocazioni di stack

Sebbene le allocazioni di stack siano spesso superiori nelle prestazioni rispetto alle allocazioni di heap nella realtà, ciò non significa certamente che le allocazioni di stack possano sempre sostituire le allocazioni di heap.

Innanzitutto, non è possibile allocare spazio nello stack con una dimensione specificata in fase di esecuzione in modo portatile con ISO C ++. Esistono estensioni fornite da implementazioni come allocaVLA (array a lunghezza variabile) di G ++, ma ci sono ragioni per evitarle. (IIRC, la fonte Linux rimuove di recente l'uso di VLA.) (Nota anche che ISO C99 ha richiesto VLA, ma ISO C11 trasforma il supporto opzionale.)

In secondo luogo, non esiste un modo affidabile e portatile per rilevare l'esaurimento dello spazio dello stack. Questo è spesso chiamato stack overflow (hmm, l'etimologia di questo sito) , ma probabilmente più precisamente, stack overrun . In realtà, questo spesso causa un accesso alla memoria non valido e lo stato del programma viene quindi danneggiato (... o forse peggio, un buco nella sicurezza). In effetti, ISO C ++ non ha il concetto di "stack" e lo rende un comportamento indefinito quando la risorsa è esaurita . Fai attenzione a quanto spazio deve essere lasciato per gli oggetti automatici.

Se lo spazio dello stack si esaurisce, ci sono troppi oggetti allocati nello stack, che possono essere causati da troppe chiamate attive di funzioni o dall'uso improprio di oggetti automatici. Tali casi possono suggerire l'esistenza di bug, ad esempio una chiamata di funzione ricorsiva senza condizioni di uscita corrette.

Tuttavia, a volte si desiderano chiamate profonde ricorsive. Nelle implementazioni di lingue che richiedono il supporto di chiamate attive non associate (dove la profondità della chiamata è limitata solo dalla memoria totale), è impossibile utilizzare lo stack di chiamate native (contemporaneo) direttamente come record di attivazione della lingua di destinazione come le tipiche implementazioni C ++. Per aggirare il problema, sono necessari modi alternativi di costruzione dei record di attivazione. Ad esempio, SML / NJ alloca esplicitamente i frame sull'heap e utilizza stack di cactus . L'allocazione complessa di tali frame di record di attivazione non è in genere rapida come i frame dello stack di chiamate. Tuttavia, se tali lingue sono ulteriormente implementate con la garanzia di corretta ricorsione della coda, l'allocazione diretta dello stack nella lingua degli oggetti (ovvero, "l'oggetto" nella lingua non viene memorizzata come riferimento, ma i valori primitivi nativi che possono essere mappati uno a uno su oggetti C ++ non condivisi) sono ancora più complicati con più penalità di prestazione in generale. Quando si utilizza C ++ per implementare tali linguaggi, è difficile stimare gli impatti sulle prestazioni.


Come stl, sempre meno sono disposti a diffondere questi concetti. Anche molti tizi su cppcon2018 usano heapfrequentemente.
力 力

@ 陳 力 "L'heap" può essere inequivocabile tenendo conto di alcune implementazioni specifiche, quindi a volte può andare bene. È ridondante "in generale", però.
FrankHB,

Che cos'è l'interoperabilità?
陳 力

@ 陳 力 Intendevo qualsiasi tipo di interoperazioni di codice "nativo" coinvolte nel sorgente C ++, ad esempio qualsiasi codice assembly inline. Ciò si basa su ipotesi (di ABI) non coperte da C ++. L'interoperabilità COM (basata su alcuni ABI specifici di Windows) è più o meno simile, sebbene sia per lo più neutra rispetto al C ++.
FrankHB,

2

C'è un punto generale da fare su tali ottimizzazioni.

L'ottimizzazione ottenuta è proporzionale alla quantità di tempo in cui il contatore del programma è effettivamente in quel codice.

Se campionate il contatore del programma, scoprirete dove trascorre il suo tempo, e che di solito si trova in una piccola parte del codice, e spesso nelle routine di libreria su cui non avete alcun controllo.

Solo se lo trovi impiegando molto tempo nell'allocazione dei tuoi oggetti, sarà notevolmente più veloce impilarli.


2

L'allocazione dello stack sarà quasi sempre più veloce o più veloce dell'allocazione dell'heap, sebbene sia certamente possibile per un allocatore dell'heap usare semplicemente una tecnica di allocazione basata sullo stack.

Tuttavia, ci sono problemi più grandi quando si tratta delle prestazioni complessive dell'allocazione basata sullo stack rispetto all'heap (o in termini leggermente migliori, allocazione locale vs. esterna). Di solito, l'allocazione (esterna) dell'heap è lenta perché ha a che fare con molti tipi diversi di allocazioni e modelli di allocazione. Ridurre l'ambito dell'allocatore che si sta utilizzando (rendendolo locale all'algoritmo / al codice) tenderà ad aumentare le prestazioni senza cambiamenti importanti. L'aggiunta di una struttura migliore ai modelli di allocazione, ad esempio forzando un ordinamento LIFO su coppie di allocazione e deallocazione, può anche migliorare le prestazioni dell'allocatore utilizzando l'allocatore in un modo più semplice e strutturato. In alternativa, è possibile utilizzare o scrivere un allocatore ottimizzato per il proprio modello di allocazione; la maggior parte dei programmi assegna frequentemente alcune dimensioni discrete, quindi un heap basato su un buffer lookaside di alcune dimensioni fisse (preferibilmente note) funzionerà estremamente bene. Windows utilizza il suo heap a bassa frammentazione proprio per questo motivo.

D'altra parte, l'allocazione basata su stack su un intervallo di memoria a 32 bit è anche piena di pericoli se si hanno troppi thread. Gli stack necessitano di un intervallo di memoria contiguo, quindi più thread hai, più spazio di indirizzi virtuale avrai bisogno per eseguirli senza un overflow dello stack. Questo non sarà un problema (per ora) con 64-bit, ma può sicuramente provocare il caos in programmi a lunga esecuzione con molti thread. L'esaurimento dello spazio degli indirizzi virtuali a causa della frammentazione è sempre un problema da affrontare.


Non sono d'accordo con la tua prima frase.
Brian Beuning,

2

Come altri hanno già detto, l'allocazione dello stack è generalmente molto più veloce.

Tuttavia, se i tuoi oggetti sono costosi da copiare, l'allocazione nello stack può comportare un enorme calo delle prestazioni in seguito quando usi gli oggetti se non stai attento.

Ad esempio, se si alloca qualcosa nello stack e lo si inserisce in un contenitore, sarebbe meglio allocare sull'heap e memorizzare il puntatore nel contenitore (ad es. Con uno std :: shared_ptr <>). La stessa cosa è vera se si stanno passando o restituendo oggetti per valore e altri scenari simili.

Il punto è che sebbene l'allocazione dello stack sia generalmente migliore dell'allocazione dell'heap in molti casi, a volte se si fa di tutto per impilare l'allocazione quando non si adatta meglio al modello di calcolo, può causare più problemi di quanti ne risolva.


2
class Foo {
public:
    Foo(int a) {

    }
}
int func() {
    int a1, a2;
    std::cin >> a1;
    std::cin >> a2;

    Foo f1(a1);
    __asm push a1;
    __asm lea ecx, [this];
    __asm call Foo::Foo(int);

    Foo* f2 = new Foo(a2);
    __asm push sizeof(Foo);
    __asm call operator new;//there's a lot instruction here(depends on system)
    __asm push a2;
    __asm call Foo::Foo(int);

    delete f2;
}

Sarebbe così in asm. Quando ci si trova func, il f1puntatore e f2è stato allocato in pila (archiviazione automatizzata). E tra l'altro, Foo f1(a1)non ha effetti sulle istruzioni stack pointer ( esp), è stato assegnato, se funcvuole ottenere il membro f1, è l'istruzione è qualcosa di simile: lea ecx [ebp+f1], call Foo::SomeFunc(). Un'altra cosa che lo stack alloca può far pensare a qualcuno che la memoria sia qualcosa del genere FIFO, è FIFOappena accaduto quando si entra in una funzione, se si è nella funzione e si alloca qualcosa del genere int i = 0, non è avvenuta alcuna spinta.


1

È stato menzionato prima che l'allocazione dello stack sta semplicemente spostando il puntatore dello stack, ovvero una singola istruzione sulla maggior parte delle architetture. Confrontalo con ciò che generalmente accade nel caso dell'allocazione dell'heap.

Il sistema operativo mantiene parti della memoria libera come un elenco collegato con i dati del payload consistenti nel puntatore all'indirizzo iniziale della porzione libera e nella dimensione della porzione libera. Per allocare X byte di memoria, l'elenco dei collegamenti viene attraversato e ogni nota viene visitata in sequenza, controllando per vedere se la sua dimensione è almeno X. Quando viene trovata una porzione con dimensione P> = X, P viene diviso in due parti con taglie X e PX. L'elenco collegato viene aggiornato e viene restituito il puntatore alla prima parte.

Come puoi vedere, l'allocazione dell'heap dipende da fattori quali la quantità di memoria richiesta, la frammentazione della memoria e così via.


1

In generale, l'allocazione dello stack è più veloce dell'allocazione dell'heap, come indicato da quasi tutte le risposte sopra. Uno stack push o pop è O (1), mentre l'allocazione o la liberazione da un heap potrebbe richiedere una camminata delle allocazioni precedenti. Tuttavia, di solito non dovresti essere allocato in loop stretti e ad alte prestazioni, quindi la scelta dipenderà di solito da altri fattori.

Potrebbe essere utile fare questa distinzione: è possibile utilizzare un "allocatore di stack" sull'heap. A rigor di termini, prendo allocazione di stack per indicare il metodo effettivo di allocazione piuttosto che la posizione dell'allocazione. Se stai allocando molte cose nello stack del programma reale, ciò potrebbe essere negativo per una serie di motivi. D'altra parte, usare un metodo stack per allocare l'heap quando possibile è la scelta migliore che puoi fare per un metodo di allocazione.

Da quando hai citato Metrowerks e PPC, immagino che intendi Wii. In questo caso, la memoria è un premio e l'utilizzo di un metodo di allocazione dello stack, ove possibile, garantisce che non si sprechi memoria sui frammenti. Naturalmente, ciò richiede molta più attenzione rispetto ai "normali" metodi di allocazione dell'heap. È saggio valutare i compromessi per ogni situazione.


1

Si noti che le considerazioni in genere non riguardano la velocità e le prestazioni nella scelta dello stack rispetto all'allocazione dell'heap. Lo stack si comporta come uno stack, il che significa che è adatto per spingere blocchi e farli scoppiare di nuovo, ultimo dentro, primo fuori. Anche l'esecuzione delle procedure è simile a una pila, l'ultima procedura immessa deve essere prima chiusa. Nella maggior parte dei linguaggi di programmazione, tutte le variabili necessarie in una procedura saranno visibili solo durante l'esecuzione della procedura, quindi vengono spinte entrando in una procedura e saltando fuori dallo stack all'uscita o al ritorno.

Ora per un esempio in cui lo stack non può essere utilizzato:

Proc P
{
  pointer x;
  Proc S
  {
    pointer y;
    y = allocate_some_data();
    x = y;
  }
}

Se si alloca un po 'di memoria nella procedura S e la si mette nello stack e quindi si esce da S, i dati allocati verranno estratti dallo stack. Ma la variabile x in P indicava anche quei dati, quindi x ora indica un punto sotto il puntatore dello stack (supponiamo che lo stack cresca verso il basso) con un contenuto sconosciuto. Il contenuto potrebbe essere ancora lì se il puntatore dello stack viene spostato verso l'alto senza cancellare i dati sottostanti, ma se si inizia a allocare nuovi dati nello stack, il puntatore x potrebbe effettivamente puntare a quei nuovi dati.


0

Non fare mai presupposti prematuri in quanto altri codici applicativi e l'utilizzo possono influire sulla tua funzione. Quindi guardare la funzione è che l'isolamento non è di alcuna utilità.

Se sei serio con l'applicazione, allora VTune o utilizzare qualsiasi strumento di profilazione simile e guardare hotspot.

Ketan


-1

Vorrei dire che in realtà il codice generato da GCC (ricordo anche VS) non ha costi generali per fare allocazione dello stack .

Dire per la seguente funzione:

  int f(int i)
  {
      if (i > 0)
      {   
          int array[1000];
      }   
  }

Di seguito è riportato il codice generato:

  __Z1fi:
  Leh_func_begin1:
      pushq   %rbp
  Ltmp0:
      movq    %rsp, %rbp
  Ltmp1:
      subq    $**3880**, %rsp <--- here we have the array allocated, even the if doesn't excited.
  Ltmp2:
      movl    %edi, -4(%rbp)
      movl    -8(%rbp), %eax
      addq    $3880, %rsp
      popq    %rbp
      ret 
  Leh_func_end1:

Quindi, ogni volta che quante variabili locali hai (anche all'interno di if o switch), solo il 3880 cambierà in un altro valore. A meno che tu non abbia una variabile locale, questa istruzione deve solo essere eseguita. Quindi allocare la variabile locale non ha un sovraccarico.

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.