std :: vector (ab) utilizza l'archiviazione automatica


46

Considera il seguente frammento:

#include <array>
int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  huge_type t;
}

Ovviamente si bloccherebbe sulla maggior parte delle piattaforme, perché la dimensione dello stack predefinita è generalmente inferiore a 20 MB.

Ora considera il seguente codice:

#include <array>
#include <vector>

int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  std::vector<huge_type> v(1);
}

Sorprendentemente si blocca anche! Il traceback (con una delle versioni libstdc ++ recenti) porta al include/bits/stl_uninitialized.hfile, dove possiamo vedere le seguenti righe:

typedef typename iterator_traits<_ForwardIterator>::value_type _ValueType;
std::fill(__first, __last, _ValueType());

Il vectorcostruttore di ridimensionamento deve inizializzare per default gli elementi, ed è così che viene implementato. Ovviamente, il _ValueType()crash temporaneo dello stack.

La domanda è se si tratta di un'implementazione conforme. Se sì, in realtà significa che l'uso di un vettore di tipi enormi è piuttosto limitato, non è vero?


Non si dovrebbero archiviare oggetti enormi in un tipo di array. Farlo potenzialmente richiede una vastissima regione di memoria contigua che potrebbe non essere presente. Invece, usa un vettore di puntatori (std :: unique_ptr in genere) in modo da non porre una domanda così alta nella tua memoria.
NathanOliver,

2
Solo memoria. Esistono implementazioni C ++ che non utilizzano memoria virtuale.
NathanOliver,

3
Quale compilatore, tra l'altro? Non riesco a riprodurre con VS 2019 (16.4.2)
ChrisMM

3
Osservando il codice libstdc ++, questa implementazione viene utilizzata solo se il tipo di elemento è banale e copia assegnabile e se std::allocatorviene utilizzato il valore predefinito .
noce

1
@Damon Come accennato in precedenza, sembra essere utilizzato solo per tipi banali con l'allocatore predefinito, quindi non dovrebbe esserci alcuna differenza osservabile.
noce

Risposte:


19

Non vi è alcun limite alla quantità di memoria automatica utilizzata da qualsiasi API std.

Tutti potrebbero richiedere 12 terabyte di spazio stack.

Tuttavia, tale API richiede solo Cpp17DefaultInsertablee l'implementazione crea un'istanza aggiuntiva rispetto a quanto richiesto dal costruttore. A meno che non sia chiuso dietro il rilevamento dell'oggetto è banalmente gestibile e copiabile, l'implementazione sembra illegale.


8
Osservando il codice libstdc ++, questa implementazione viene utilizzata solo se il tipo di elemento è banale e copia assegnabile e se std::allocatorviene utilizzato il valore predefinito . Non sono sicuro del motivo per cui questo caso speciale sia stato realizzato in primo luogo.
noce

3
@walnut Il che significa che il compilatore è libero di creare se non effettivamente l'oggetto temporaneo; Immagino che ci sia una buona possibilità su una build ottimizzata che non viene creata?
Yakk - Adam Nevraumont

4
Sì, credo che potrebbe, ma per elementi di grandi dimensioni GCC non sembra. Clang con libstdc ++ ottimizza il temporaneo, ma sembra solo se la dimensione del vettore passato al costruttore è una costante di compilazione, vedi godbolt.org/z/-2ZDMm .
noce

1
@walnut il caso speciale è lì in modo che inviamo std::fillper tipi banali, che quindi utilizza memcpyper far esplodere i byte in luoghi, che è potenzialmente molto più veloce rispetto alla costruzione di molti singoli oggetti in un ciclo. Credo che l'implementazione di libstdc ++ sia conforme, ma causare un overflow dello stack per oggetti enormi è un bug di Quality of Implementation (QoI). L'ho segnalato come gcc.gnu.org/PR94540 e lo risolverò.
Jonathan Wakely,

@JonathanWakely Sì, ha senso. Non ricordo perché non ci abbia pensato quando ho scritto il mio commento. Immagino che avrei pensato che il primo elemento costruito per impostazione predefinita sarebbe stato costruito direttamente sul posto e quindi uno avrebbe potuto copiarlo da quello, in modo che nessun oggetto aggiuntivo del tipo di elemento fosse mai costruito. Ma ovviamente non ci ho pensato in dettaglio e non conosco i dettagli dell'implementazione della libreria standard. (Mi sono reso conto troppo tardi che questo è anche il tuo suggerimento nella segnalazione di bug.)
noce,

9
huge_type t;

Ovviamente andrebbe in crash sulla maggior parte delle piattaforme ...

Contesto l'assunto di "maggior parte". Poiché la memoria dell'enorme oggetto non viene mai utilizzata, il compilatore può ignorarlo completamente e non allocare mai la memoria, nel qual caso non si verifica alcun arresto anomalo.

La domanda è se si tratta di un'implementazione conforme.

Lo standard C ++ non limita l'uso dello stack né riconosce l'esistenza di uno stack. Quindi sì, è conforme allo standard. Ma si potrebbe considerare che questo sia un problema di qualità dell'attuazione.

significa in realtà che l'uso di un vettore di tipi enormi è piuttosto limitato, no?

Questo sembra essere il caso di libstdc ++. L'incidente non è stato riprodotto con libc ++ (usando clang), quindi sembra che questo non sia un limite nel linguaggio, ma piuttosto solo in quella particolare implementazione.


6
"non si bloccherà necessariamente nonostante l'overflow dello stack perché il programma non accede mai alla memoria allocata" - se lo stack viene utilizzato in qualche modo dopo questo (ad es. per chiamare una funzione), si arresta in modo anomalo anche sulle piattaforme con commit eccessivo .
Ruslan

Qualsiasi piattaforma su cui ciò non si arresta in modo anomalo (presupponendo che l'oggetto non sia allocato correttamente) è vulnerabile a Stack Clash.
user253751

@ user253751 Sarebbe ottimista presumere che la maggior parte delle piattaforme / programmi non siano vulnerabili.
Eerorika,

Penso che il sovraccarico si applichi solo all'heap, non allo stack. La pila ha un limite superiore fisso sulla sua dimensione.
Jonathan Wakely, il

@JonathanWakely Hai ragione. Sembra che il motivo per cui non si arresta in modo anomalo è perché il compilatore non alloca mai l'oggetto inutilizzato.
Eerorika,

5

Non sono un avvocato linguista né un esperto di standard C ++, ma cppreference.com dice:

explicit vector( size_type count, const Allocator& alloc = Allocator() );

Crea il contenitore con il numero di istanze di T. inserite per impostazione predefinita. Non vengono eseguite copie.

Forse sto fraintendendo "inserito per impostazione predefinita", ma mi aspetterei:

std::vector<huge_type> v(1);

essere equivalente a

std::vector<huge_type> v;
v.emplace_back();

L'ultima versione non dovrebbe creare una copia dello stack ma costruire un tipo enorme direttamente nella memoria dinamica del vettore.

Non posso dire autorevolmente che ciò che vedi non è conforme, ma non è certamente quello che mi aspetterei da un'implementazione di qualità.


4
Come ho detto in un commento sulla domanda, libstdc ++ usa questa implementazione solo per tipi banali con assegnazione di copie e std::allocator, quindi, non dovrebbe esserci alcuna differenza osservabile tra l'inserimento diretto nella memoria dei vettori e la creazione di una copia intermedia.
Noce

@walnut: giusto, ma l'enorme allocazione dello stack e l'impatto sulle prestazioni di init e copia sono ancora cose che non mi aspetterei da un'implementazione di alta qualità.
Adrian McCarthy,

2
Si, sono d'accordo. Penso che questa sia stata una svista nell'attuazione. Il mio punto era solo che non importa in termini di conformità standard.
noce

IIRC è inoltre necessario copiare o spostare emplace_backma non solo per creare un vettore. Ciò significa che puoi avere, vector<mutex> v(1)ma non vector<mutex> v; v.emplace_back();per qualcosa come, huge_typepotresti avere ancora un'allocazione e spostare di più l'operazione con la seconda versione. Nessuno dei due dovrebbe creare oggetti temporanei.
domenica

1
@IgorR. vector::vector(size_type, Allocator const&)richiede (Cpp17) DefaultInsertable
dyp
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.