Anfangskapazität des Vektors in C ++

89

Was ist das capacity()von einem, std::vectordas mit dem Standardkonstruktor erstellt wird? Ich weiß, dass das size()Null ist. Können wir feststellen, dass ein standardmäßig konstruierter Vektor keine Heap-Speicherzuordnung aufruft?

Auf diese Weise wäre es möglich, ein Array mit einer beliebigen Reserve unter Verwendung einer einzelnen Zuordnung zu erstellen, wie z std::vector<int> iv; iv.reserve(2345);. Nehmen wir an, ich möchte aus irgendeinem Grund die size()2345 nicht starten .

Zum Beispiel unter Linux (g ++ 4.4.5, Kernel 2.6.32 amd64)

#include <iostream>
#include <vector>

int main()
{
  using namespace std;
  cout << vector<int>().capacity() << "," << vector<int>(10).capacity() << endl;
  return 0;
}

gedruckt 0,10. Ist es eine Regel oder ist es vom STL-Anbieter abhängig?

Notinlist
quelle
7
Standard gibt nichts über die Anfangskapazität des Vektors an, aber die meisten Implementierungen verwenden 0.
Mr. Anubis
11
Es gibt keine Garantie, aber ich würde die Qualität einer Implementierung, die Speicher zugewiesen hat, ernsthaft in Frage stellen, ohne dass ich eine anfordere.
Mike Seymour
2
@ MikeSeymour nicht einverstanden. Eine wirklich leistungsstarke Implementierung kann einen kleinen Inline-Puffer enthalten. In diesem Fall wäre es sinnvoll, die anfängliche Kapazität () darauf einzustellen.
Alastair
6
@alastair Bei Verwendung swapbleiben alle Iteratoren und Referenzen gültig (außer end()s). Das bedeutet, dass ein Inline-Puffer nicht möglich ist.
Notinlist

Antworten:

71

Der Standard gibt nicht an, wie die Initiale capacityeines Containers lauten soll, daher verlassen Sie sich auf die Implementierung. Bei einer gemeinsamen Implementierung wird die Kapazität bei Null gestartet, es gibt jedoch keine Garantie. Auf der anderen Seite gibt es keine Möglichkeit, Ihre Strategie zu std::vector<int> iv; iv.reserve(2345);verbessern, sich daran zu halten.

Mark Ransom
quelle
1
Ich kaufe deine letzte Aussage nicht. Wenn Sie sich nicht darauf verlassen können, dass die Kapazität anfänglich 0 ist, können Sie Ihr Programm so umstrukturieren, dass Ihr Vektor eine anfängliche Größe hat. Dies würde die Hälfte der Anzahl von Heap-Speicheranforderungen (von 2 bis 1) bedeuten.
Bitmaske
4
@bitmask: Praktisch sein: Kennen Sie eine Implementierung, bei der ein Vektor Speicher im Standardkonstruktor zuweist ? Es ist nicht durch den Standard garantiert, aber wie Mike Seymour betont, wäre das Auslösen einer Zuweisung ohne die Notwendigkeit ein schlechter Geruch in Bezug auf die Qualität der Implementierung .
David Rodríguez - Dribeas
3
@ DavidRodríguez-dribeas: Darum geht es nicht. Die Prämisse lautete: "Sie können es nicht besser machen als Ihre derzeitige Strategie. Fragen Sie sich also nicht, ob es möglicherweise dumme Implementierungen gibt." Wenn die Prämisse lautete: "Es gibt keine solchen Implementierungen, also stören Sie sich nicht", würde ich sie kaufen. Die Schlussfolgerung ist zwar wahr, aber die Implikation funktioniert nicht. Entschuldigung, vielleicht pflücke ich nicht.
Bitmaske
3
@bitmask Wenn es eine Implementierung gibt, die bei der Standardkonstruktion Speicher zuweist, würde das, was Sie gesagt haben, die Anzahl der Zuweisungen halbieren. Dies vector::reserveist jedoch nicht dasselbe wie die Angabe einer Anfangsgröße. Die Vektorkonstruktoren, die einen anfänglichen Größenwert / eine Kopie annehmen, initialisieren nObjekte und weisen daher eine lineare Komplexität auf. OTOH bedeutet das Aufrufen von Reserve nur das Kopieren / Verschieben von size()Elementen, wenn eine Neuzuweisung ausgelöst wird. Auf einem leeren Vektor gibt es nichts zu kopieren. Letzteres kann daher auch dann wünschenswert sein, wenn die Implementierung Speicher für einen standardmäßig konstruierten Vektor zuweist.
Prätorianer
4
@bitmask, wenn Sie über die Zuordnungen zu diesem Grad besorgt sind, sollten Sie sich die Implementierung Ihrer speziellen Standardbibliothek ansehen und sich nicht auf Spekulationen verlassen.
Mark Ransom
34

Die Speicherimplementierungen von std :: vector variieren erheblich, aber alle, auf die ich gestoßen bin, beginnen bei 0.

Der folgende Code:

#include <iostream>
#include <vector>

int main()
{
  using namespace std;

  vector<int> normal;
  cout << normal.capacity() << endl;

  for (unsigned int loop = 0; loop != 10; ++loop)
  {
      normal.push_back(1);
      cout << normal.capacity() << endl;
  }

  cin.get();
  return 0;
}

Gibt die folgende Ausgabe:

0
1
2
4
4
8
8
8
8
16
16

unter GCC 5.1 und:

0
1
2
3
4
6
6
9
9
9
13

unter MSVC 2013.

Metamorphose
quelle
3
Dies ist so unterschätzt @ Andrew
Valentin Mercier
Nun, Sie finden praktisch überall, dass die Empfehlung für Geschwindigkeitszwecke fast immer darin besteht, nur einen Vektor zu verwenden. Wenn Sie also etwas tun, das spärliche Daten beinhaltet ...
Andrew
@ Andrew, bei was hätten sie damit anfangen sollen? Das Zuweisen von Daten würde nur Zeit verschwenden, wenn dieser Speicher reserviert und freigegeben wird, wenn der Programmierer mehr als den Standard reservieren möchte. Wenn Sie davon ausgehen, dass sie mit 1 beginnen sollen, wird dies zugewiesen, sobald jemand ohnehin 1 zuweist.
Pfütze
@Puddle Sie lesen zwischen den Zeilen, anstatt sie zum Nennwert zu nehmen. Der Hinweis, dass es kein Sarkasmus ist, ist das Wort "klug" sowie mein zweiter Kommentar, in dem spärliche Daten erwähnt werden.
Andrew
@ Andrew Oh gut, du warst erleichtert genug, dass sie es bei 0 angefangen haben. Warum überhaupt scherzhaft darüber kommentieren?
Pfütze
6

Soweit ich den Standard verstanden habe (obwohl ich eigentlich keine Referenz nennen konnte), wurden die Instanziierung von Containern und die Speicherzuweisung aus gutem Grund absichtlich entkoppelt. Dafür haben Sie unterschiedliche, separate Anrufe für

  • constructor um den Container selbst zu erstellen
  • reserve() einen entsprechend großen Speicherblock vorab zuzuweisen, um mindestens (!) eine gegebene Anzahl von Objekten aufzunehmen

Und das macht sehr viel Sinn. Das einzige Existenzrecht reserve()besteht darin, Ihnen die Möglichkeit zu geben, beim Wachsen des Vektors möglicherweise teure Neuzuweisungen zu codieren. Um nützlich zu sein, müssen Sie die Anzahl der zu speichernden Objekte kennen oder zumindest eine fundierte Vermutung anstellen können. Wenn dies nicht gegeben ist, halten Sie sich besser fern, reserve()da Sie nur die Neuzuweisung für verschwendeten Speicher ändern.

Also alles zusammen:

  • Der Standard absichtlich nicht nicht geben Sie einen Konstruktor, der Sie einen Speicherblock für eine bestimmte Anzahl von Objekten im Voraus ermöglicht zuweisen (die zumindest wünschenswerter wäre eine Implementierung spezifische, feste „etwas“ unter der Haube als Zuweisung).
  • Die Zuordnung sollte nicht implizit sein. Um einen Block vorab zuzuweisen, müssen Sie einen separaten Anruf tätigen, reserve()und dieser muss sich nicht am selben Bauort befinden (könnte / sollte natürlich später sein, nachdem Sie die erforderliche Größe für die Unterbringung erkannt haben).
  • Wenn also ein Vektor immer einen Speicherblock mit definierter Größe der Implementierung vorab zuweisen würde reserve(), würde dies den beabsichtigten Job vereiteln , nicht wahr ?
  • Was wäre der Vorteil einer Vorbelegung eines Blocks, wenn die STL den beabsichtigten Zweck und die erwartete Größe eines Vektors natürlich nicht kennen könnte? Es wird eher unsinnig, wenn nicht kontraproduktiv sein.
  • Die richtige Lösung besteht stattdessen darin, dem ersten Block einen bestimmten Block zuzuweisen und zu implementieren push_back()- sofern dies nicht bereits zuvor explizit zugewiesen wurde reserve().
  • Im Falle einer notwendigen Neuzuweisung ist die Erhöhung der Blockgröße ebenfalls implementierungsspezifisch. Die mir bekannten Vektorimplementierungen beginnen mit einer exponentiellen Zunahme der Größe, begrenzen jedoch die Inkrementierungsrate auf ein bestimmtes Maximum, um zu vermeiden, dass große Mengen an Speicher verschwendet oder sogar ausgeblasen werden.

All dies kommt nur dann zum vollen Betrieb und zum Vorteil, wenn es nicht durch einen zuweisenden Konstruktor gestört wird. Sie haben angemessene Standardeinstellungen für gängige Szenarien, die bei Bedarf von reserve()(und shrink_to_fit()) überschrieben werden können . Selbst wenn der Standard dies nicht explizit angibt, bin ich mir ziemlich sicher, dass die Annahme, dass ein neu konstruierter Vektor nicht vorbelegt wird, eine ziemlich sichere Wette für alle aktuellen Implementierungen ist.

Don Pedro
quelle
4

Als kleine Ergänzung zu den anderen Antworten stellte ich fest, dass beim Ausführen unter Debug-Bedingungen mit Visual Studio ein standardmäßig erstellter Vektor dem Heap weiterhin zugewiesen wird, obwohl die Kapazität bei Null beginnt.

Insbesondere wenn _ITERATOR_DEBUG_LEVEL! = 0 ist, weist der Vektor etwas Platz zu, um bei der Iteratorprüfung zu helfen.

https://docs.microsoft.com/en-gb/cpp/standard-library/iterator-debug-level

Ich fand das nur etwas ärgerlich, da ich zu diesem Zeitpunkt einen benutzerdefinierten Allokator verwendete und nicht mit der zusätzlichen Allokation rechnete.

David Woo
quelle
Interessant, brechen sie die noexcept-Garantien (zumindest für C + 17, früher?): En.cppreference.com/w/cpp/container/vector/vector
Deduplicator
3

Dies ist eine alte Frage, und alle Antworten hier haben den Standpunkt des Standards und die Art und Weise, wie Sie eine anfängliche Kapazität auf tragbare Weise erhalten können, zu Recht erklärt std::vector::reserve.

Ich werde jedoch erklären, warum es für eine STL-Implementierung nicht sinnvoll ist, beim Erstellen eines std::vector<T>Objekts Speicher zuzuweisen .

  1. std::vector<T> von unvollständigen Typen;

    Vor C ++ 17 war es ein undefiniertes Verhalten, ein zu erstellen, std::vector<T>wenn die Definition von zum TZeitpunkt der Instanziierung noch unbekannt ist. Diese Einschränkung wurde jedoch in C ++ 17 gelockert .

    Um Speicher für ein Objekt effizient zuzuweisen, müssen Sie dessen Größe kennen. Ab C ++ 17 und darüber hinaus haben Ihre Clients möglicherweise Fälle, in denen Ihre std::vector<T>Klasse die Größe von nicht kennt T. Ist es sinnvoll, Speicherzuordnungsmerkmale abhängig von der Typvollständigkeit zu haben?

  2. Unwanted Memory allocations

    Es gibt viele, viele, viele Male, in denen Sie ein Diagramm in der Software modellieren müssen. (Ein Baum ist ein Graph); Sie werden es höchstwahrscheinlich wie folgt modellieren:

    class Node {
        ....
        std::vector<Node> children; //or std::vector< *some pointer type* > children;
        ....
     };

    Denken Sie jetzt einen Moment nach und stellen Sie sich vor, Sie hätten viele Endknoten. Sie wären sehr sauer, wenn Ihre STL-Implementierung zusätzlichen Speicherplatz zuweist, nur um Objekte zu haben children.

    Dies ist nur ein Beispiel, denken Sie an mehr ...

WhiZTiM
quelle
2

Standard gibt keinen Anfangswert für die Kapazität an, aber der STL-Container nimmt automatisch zu, um so viele Daten aufzunehmen, wie Sie eingegeben haben, vorausgesetzt, Sie überschreiten nicht die maximale Größe (verwenden Sie die Mitgliedsfunktion max_size, um dies zu wissen). Bei Vektoren und Strings wird das Wachstum von Realloc verwaltet, wenn mehr Platz benötigt wird. Angenommen, Sie möchten einen Vektor mit dem Wert 1-1000 erstellen. Ohne Verwendung von Reserve führt der Code in der Regel zu 2 bis 18 Neuzuweisungen während der folgenden Schleife:

vector<int> v;
for ( int i = 1; i <= 1000; i++) v.push_back(i);

Das Ändern des Codes zur Verwendung von Reserve kann zu 0 Zuweisungen während der Schleife führen:

vector<int> v;
v.reserve(1000);

for ( int i = 1; i <= 1000; i++) v.push_back(i);

Grob gesagt wachsen die Vektor- und String-Kapazitäten jedes Mal um den Faktor 1,5 bis 2.

Archie Yalakki
quelle