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 new
chiamate 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
/ POP
istruzioni).
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 alloca
VLA (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.