2011-01-28 14 views
13

L'operatore new (o per POD, malloc/calloc) supporta una forma semplice ed efficiente di errore durante l'allocazione di grandi blocchi di memoria.I contenitori di libreria standard (STL) supportano una forma di allocazione di nothrow?

Dire che abbiamo questo:

const size_t sz = GetPotentiallyLargeBufferSize(); // 1M - 1000M 
T* p = new (nothrow) T[sz]; 
if(!p) { 
    return sorry_not_enough_mem_would_you_like_to_try_again; 
} 
... 

C'è un tale costrutto per le std :: contenitori, o avrà devo sempre gestire un'eccezione (previsto !!) con std::vector e gli amici?


Sarebbe là forse essere un modo per scrivere un allocatore personalizzato che prealloca la memoria e poi passare questo allocatore personalizzato al vettore, in modo che, fintanto che il vettore non chiede più memoria di quanto messo in allocatore in anticipo, non fallirà?


Ripensamento: Ciò che veramente sarebbe necessario fosse una funzione membro bool std::vector::reserve(std::nothrow) {...} in aggiunta alla funzione di riserva normale. Ma dal momento che ciò avrebbe senso solo se gli allocatori fossero stati estesi anche per consentire l'allocazione del nothrow, non accadrà. Sembra (nothrow) nuovo è buono per qualcosa, dopo tutto :-)


Edit: Per quanto riguarda il motivo per cui sto chiedendo anche questo:

ho pensato a questa domanda durante il debug (1a possibilità/Gestione delle eccezioni della seconda possibilità del debugger): Se ho impostato il mio debugger su 1st-chance catch any bad_alloc perché sto testando per condizioni di memoria bassa, sarebbe fastidioso se anche catturasse quelle eccezioni bad_alloc che sono già buone atteso e gestito nel codice. Non era/non è un grosso problema ma mi è venuto in mente che il sermone dice che le eccezioni sono per circostanze eccezionali, e qualcosa che già mi aspetto di accadere ogni strana chiamata nel codice non è eccezionale.

Se new (nothrow) ha i suoi usi legittimi, avrebbe anche una riserva di vettori-nothrow.

+3

Non è un grosso problema scrivere un costrutto 'try ... catch'. Se ne possiedi molti, potresti scrivere una 'funzione di wrapper singolo' modello T * MyAlloc (size_t) ', con la gestione delle eccezioni all'interno. – TonyK

+1

a volte è un grosso problema. –

+0

anche quando assegni un gigabyte e ti avvicini a OOM? Ne dubito. –

risposta

14

Per impostazione predefinita, le classi di contenitore STL standard utilizzano la classe std::allocator sotto il cofano per fare la loro allocazione, motivo per cui possono lanciare std::bad_alloc se non c'è memoria disponibile. È interessante notare che la specifica ISO C++ sugli allocatori afferma che il valore restituito di qualsiasi tipo di allocatore deve essere un puntatore a un blocco di memoria in grado di contenere un numero di elementi, che impedisce automaticamente all'utente di creare un allocatore personalizzato che potrebbe potenzialmente utilizzare il nothrow versione di new per avere questi tipi di errori di allocazione non presidiata. Tuttavia, è possibile creare un allocatore personalizzato che interrompe il programma se non è disponibile memoria, poiché è vero che la memoria restituita è valida quando non viene lasciata memoria. :-)

In breve, i contenitori standard generano eccezioni per impostazione predefinita e in qualsiasi modo si possa provare a personalizzarle con un allocatore personalizzato per evitare che le eccezioni vengano generate non saranno conformi alle specifiche.

+2

La maggior parte dei metodi che chiamano allocatore non hanno altro modo per segnalare comunque l'errore che tramite eccezione. –

5

Troppo spesso sentiamo "Non voglio usare eccezioni perché sono inefficienti".

A meno che non ci si riferisca ad un ambiente "incorporato" in cui si desidera disattivare tutte le informazioni sul tipo runtime, non si dovrebbe preoccupare troppo dell'inefficienza delle eccezioni se vengono lanciate in modo appropriato.L'esaurimento della memoria è uno di questi modi appropriati.

Parte del contratto di vettore è che genererà se non può allocare. Se si scrive un allocatore personalizzato che restituisce NULL invece che sarebbe peggio, in quanto causerebbe un comportamento indefinito.

Se si deve quindi utilizzare un allocatore che tenterà prima un callback di allocazione non riuscita se uno è disponibile, e solo allora se ancora non è possibile allocare per il lancio, ma comunque si deve finire con un'eccezione.

posso darti un suggerimento però: se davvero assegnano tali grandi quantità di dati, allora vettore è probabilmente la classe sbagliata da usare e si dovrebbe usare std :: deque invece. Perché? Perché deque non richiede un blocco contiguo di memoria ma è ancora una ricerca costante di tempo. E i vantaggi sono duplici:

    • allocazioni non riusciranno meno frequentemente. Perché non hai bisogno di un blocco contiguo, quindi potresti avere la memoria disponibile anche se non in un singolo blocco.
    • Non c'è riallocazione, solo più allocazioni. Le riallocazioni sono costose in quanto richiedono il trasferimento di tutti gli oggetti. Quando si è in modalità volume elevato può essere un'operazione molto tempestiva.

Quando ho lavorato su un sistema del genere in passato abbiamo scoperto che potevamo effettivamente memorizzati più di 4 volte i dati tanto utilizzando deque come abbiamo potuto utilizzare vettore a causa di sopra della ragione 1, ed è stato più veloce perché del motivo 2.

Qualcos'altro che abbiamo fatto è stato allocare un buffer di riserva da 2MB e quando abbiamo preso un bad_alloc abbiamo liberato il buffer e poi abbiamo gettato comunque per mostrare che avevamo raggiunto la capacità. Ma con 2MB di riserva ora sapevamo almeno che avevamo la memoria per eseguire piccole operazioni per spostare i dati dalla memoria all'archiviazione temporanea su disco.

Così potremmo prendere il bad_alloc a volte e prendere un'azione appropriata mantenendo uno stato coerente, che è lo scopo delle eccezioni piuttosto che assumere che esaurire la memoria è sempre fatale e non dovrebbe mai fare altro che terminare il programma (o anche peggio, invocare un comportamento indefinito).

+0

Grazie per la dritta su 'deque' - osservazione interessante, potrebbe valere la pena di provare. (Anche se, come suggerisce il mio codice originale, se qualcuno richiede un buffer> 500 MB, è molto probabile che fallisca il più delle volte indipendentemente da come lo si frammenta.) –

+0

@Martin: perché è probabile che un deque' 500MG fallisca? Il punto di 'deque' è che non è un singolo buffer, sono molte allocazioni. Ad esempio, se si utilizza un PC ancora vagamente moderno, è necessario assegnare comodamente 500 MB in piccoli blocchi. Più di circa 3 allocazioni di questo tipo potrebbero iniziare a complicarsi su un sistema operativo/processo a 32 bit. –

+0

@Steve - Sì, processo a 32 bit. Non è la RAM fisica che si sta esaurendo, ma lo spazio degli indirizzi del processo sta diventando "pieno". Un problema che vedrei con un 'deque' è che non si può essere troppo sicuri di come suddividerà il buffer. Quindi potresti avere situazioni in cui funziona bene e altri scenari (di frammentazione) in cui non funziona affatto. Potrebbe essere una domanda interessante di per sé. –

2

I contenitori standard utilizzano eccezioni per questo, non è possibile aggirarlo diverso dal tentativo di allocazione solo una volta che si è certi che avrà esito positivo. Non è possibile farlo portabilmente, perché l'implementazione verrà sovra-allocata in genere da un importo non specificato. Se devi disabilitare le eccezioni nel compilatore, allora sei limitato a ciò che puoi fare con i contenitori.

Per quanto riguarda la "semplice ed efficiente", penso che il std contenitori sono abbastanza semplici e ragionevolmente efficiente:

T* p = new (nothrow) T[sz]; 
if(!p) { 
    return sorry_not_enough_mem_would_you_like_to_try_again; 
} 
... more code that doesn't throw ... 
delete[] p; 

try { 
    std::vector<T> p(sz); 
    ... more code that doesn't throw ... 
} catch (std::bad_alloc) { 
    return sorry_not_enough_mem_would_you_like_to_try_again; 
} 

E 'lo stesso numero di righe di codice. Se presenta un problema di efficienza nel caso di errore, il tuo programma deve fallire centinaia di migliaia di volte al secondo, nel qual caso metto un po 'in dubbio la progettazione del programma. Mi chiedo anche in quali circostanze il costo del lancio e della cattura di un'eccezione è significativo rispetto al costo della chiamata di sistema che lo new probabilmente fa stabilire che non può soddisfare la richiesta.

Ma ancora meglio, che ne dite di scrivere le API per utilizzare le eccezioni troppo:

std::vector<T> p(sz); 
... more code that doesn't throw ... 

Quattro linee più corte di quanto il tuo codice originale, e il chiamante che ha attualmente a gestire "sorry_not_enough_mem_would_you_like_to_try_again" può invece gestire l'eccezione. Se questo codice di errore viene passato attraverso diversi livelli di chiamanti, è possibile salvare quattro righe per ogni livello. C++ ha delle eccezioni, e per quasi tutti gli scopi si può anche accettare questo e scrivere il codice di conseguenza.

Riguardo a "(atteso !!)" - a volte si sa come gestire una condizione di errore. La cosa da fare in questo caso è prendere l'eccezione. È come dovrebbero funzionare le eccezioni. Se il codice che genera l'eccezione in qualche modo sapeva che non era il caso che qualcuno lo catturasse, allora avrebbe potuto terminare il programma.

+0

Per quanto riguarda la tua proposta di fare in modo che l'API propaghi l'eccezione: penso che propagare bad_alloc quando sai già quale allocazione di dimensioni critiche potrebbe fallire è un'idea abbastanza brutta. Qualunque sia il codice che prenderà, non sarebbe più saggio dove o quale allocazione è fallita, e se già sai che stai facendo allocazioni di dimensioni critiche, allora penso che sia davvero meglio prendere cattiva informazione e rilanciare qualcosa con più informazioni/qualcosa che possa essere preso + gestito in un modo più specifico. –

+0

@ Martin: bene, conosci il tuo caso d'uso specifico e io no. Se il codice richiede la memoria, allora non vedo come la sua necessità di 1 GB di memoria sia sostanzialmente diversa dal codice che richiede 1 byte di memoria (allocata dinamicamente). Normalmente, "riprovare" non è una buona risposta alla memoria esaurita, ma se nel tuo caso specifico ci sono alcune allocazioni per le quali solo provare di nuovo potrebbe funzionare, e altre per le quali non funzionerà, allora distinguere con tutti i mezzi il due. –

Problemi correlati