In diesem Artikel möchte ich einfache Beispiele für die Arbeit mit Komponenten aus dem pmr- Namespace und die Grundideen zeigen, die polymorphen Allokatoren zugrunde liegen.
Die Hauptidee der in c ++ 17 eingeführten polymorphen Allokatoren besteht darin, die auf Basis des statischen Polymorphismus oder mit anderen Worten Vorlagen implementierten Standardallokatoren zu verbessern. Sie sind viel einfacher zu verwenden als Standard-Allokatoren. Außerdem können Sie den Containertyp beibehalten, wenn Sie verschiedene Allokatoren verwenden, und daher die Allokatoren zur Laufzeit ändern.
Wenn Sie
std::vectoreinen bestimmten Speicherzuweiser verwenden möchten , können Sie den Vorlagenparameter Allocator verwenden:
auto my_vector = std::vector<int, my_allocator>();
Es gibt jedoch ein Problem: Dieser Vektor ist nicht vom gleichen Typ wie ein Vektor mit einem anderen Allokator, einschließlich eines standardmäßig definierten.
Ein solcher Container kann weder an eine Funktion übergeben werden, für die ein Vektor mit einem Standardcontainer erforderlich ist, noch können zwei Vektoren mit unterschiedlichen Zuordnungstypen derselben Variablen zugewiesen werden, z. B.:
auto my_vector = std::vector<int, my_allocator>();
auto my_vector2 = std::vector<int, other_allocator>();
auto vec = my_vector; // ok
vec = my_vector2; // error
Ein polymorpher Allokator enthält einen Zeiger auf eine Schnittstelle,
memory_resourcedamit er den dynamischen Versand verwenden kann.
Um die Strategie der Arbeit mit dem Speicher zu ändern, reicht es aus, die Instanz zu ersetzen
memory_resourceund den Typ des Allokators beizubehalten. Dies kann auch zur Laufzeit erfolgen. Ansonsten arbeiten polymorphe Allokatoren nach den gleichen Regeln wie Standard-Allokatoren.
Die spezifischen Datentypen, die vom neuen Allokator verwendet werden, befinden sich im Namespace
std::pmr. Es gibt auch Vorlagenspezialisierungen von Standardcontainern, die mit einem polymorphen Allokator arbeiten können.
Eines der Hauptprobleme im Moment ist die Inkompatibilität neuer Versionen von Containern
std::pmrmit Analoga von std.
Hauptbestandteile std::pmr:
std::pmr::memory_resource— , .- :
virtual void* do_allocate(std::size_t bytes, std::size_t alignment),virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment)virtual bool do_is_equal(const std::pmr::memory_resource& other) const noexcept.
std::pmr::polymorphic_allocator— ,memory_resource.new_delete_resource()null_memory_resource()«»- :
synchronized_pool_resourceunsynchronized_pool_resourcemonotonic_buffer_resource
- ,
std::pmr::vector,std::pmr::string,std::pmr::map. , . -
memory_resource:
memory_resource* new_delete_resource(), memory_resource, new delete .memory_resource* null_memory_resource()
Die freie Funktion gibt einen Zeiger zurück, aufmemory_resourcedenstd::bad_allocbei jedem Zuweisungsversuch eine Ausnahme ausgelöst wird.
Dies kann nützlich sein, um sicherzustellen, dass Objekte keinen Speicher auf dem Heap zuweisen oder zu Testzwecken.
class synchronized_pool_resource : public std::pmr::memory_resource
Eine thread-sichere Allzweck-Implementierung von memory_resource besteht aus einer Reihe von Pools mit unterschiedlichen Größen von Speicherblöcken.
Jeder Pool ist eine Sammlung von Speicherblöcken gleicher Größe.class unsynchronized_pool_resource : public std::pmr::memory_resource
Single-Threaded-Versionsynchronized_pool_resource.class monotonic_buffer_resource : public std::pmr::memory_resource
Single-Threaded, schnell, fürmemory_resourcespezielle Zwecke, nimmt Speicher aus einem vorab zugewiesenen Puffer, gibt ihn jedoch nicht frei, dh er kann nur wachsen.
Anwendungsbeispiel
monotonic_buffer_resourceund pmr::vector:
#include <iostream>
#include <memory_resource> // pmr core types
#include <vector> // pmr::vector
#include <string> // pmr::string
int main() {
char buffer[64] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
std::cout << buffer << '\n';
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
std::pmr::vector<char> vec{ &pool };
for (char ch = 'a'; ch <= 'z'; ++ch)
vec.push_back(ch);
std::cout << buffer << '\n';
}
Programmausgabe:
_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______
Im obigen Beispiel haben wir
monotonic_buffer_resourceinitialisiert mit einem auf dem Stapel zugewiesenen Puffer verwendet. Mit einem Zeiger auf diesen Puffer können wir den Speicherinhalt einfach anzeigen.
Der Vektor nimmt Speicher aus dem Pool, was sehr schnell ist, da er sich auf dem Stapel befindet. Wenn ihm der Speicher ausgeht, fordert er ihn mithilfe des globalen Operators an
new. Das Beispiel zeigt eine Vektorimplementierung, wenn versucht wird, mehr als die reservierte Anzahl von Elementen einzufügen. In diesem Fall wird der monotonic_bufferalte Speicher nicht freigegeben, sondern wächst nur.
Sie können natürlich
reserve()einen Vektor aufrufen , um Neuzuordnungen zu minimieren. Der Zweck des Beispiels besteht jedoch darin, genau zu demonstrieren, wie er sich ändert, monotonic_buffer_resourcewenn sich der Container erweitert.
Lager pmr::string
Was ist, wenn wir Strings speichern möchten
pmr::vector?
Ein wichtiges Merkmal ist, dass Objekte in einem Container, die auch einen polymorphen Allokator verwenden, den Allokator des übergeordneten Containers für die Speicherverwaltung anfordern.
Wenn Sie diese Funktion nutzen möchten, müssen Sie sie
std::pmr::stringstattdessen verwenden std::string.
Betrachten wir ein Beispiel mit einem Puffer auf dem Stack vorab zugewiesene Stellen , die wir passieren , wie
memory_resourcefür std::pmr::vector std::pmr::string:
#include <iostream>
#include <memory_resource> // pmr core types
#include <vector> // pmr::vector
#include <string> // pmr::string
int main() {
std::cout << "sizeof(std::string): " << sizeof(std::string) << '\n';
std::cout << "sizeof(std::pmr::string): " << sizeof(std::pmr::string) << '\n';
char buffer[256] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
const auto BufferPrinter = [](std::string_view buf, std::string_view title) {
std::cout << title << ":\n";
for (auto& ch : buf) {
std::cout << (ch >= ' ' ? ch : '#');
}
std::cout << '\n';
};
BufferPrinter(buffer, "zeroed buffer");
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
std::pmr::vector<std::pmr::string> vec{ &pool };
vec.reserve(5);
vec.push_back("Hello World");
vec.push_back("One Two Three");
BufferPrinter(std::string_view(buffer, std::size(buffer)), "after two short strings");
vec.emplace_back("This is a longer string");
BufferPrinter(std::string_view(buffer, std::size(buffer)), "after longer string strings");
vec.push_back("Four Five Six");
BufferPrinter(std::string_view(buffer, std::size(buffer)), "after the last string");
}
Programmausgabe:
sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________#
after longer string strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________________________________________________________________________________________This is a longer string#_______________________________#
after the last string:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________#m### ###n### ##########Four Five Six###________________________________________This is a longer string#_______________________________#
Die wichtigsten Punkte, die in diesem Beispiel zu beachten sind:
- Die Größe ist
pmr::stringgrößer alsstd::string. Dies liegt an der Tatsache, dass ein Zeiger aufmemory_resource; - Wir reservieren den Vektor für 5 Elemente, sodass beim Hinzufügen von 4 keine Neuzuordnungen auftreten.
- Die ersten beiden Zeilen sind kurz genug für den Vektorspeicherblock, sodass keine zusätzliche Speicherzuordnung erfolgt.
- Die dritte Zeile ist länger und erfordert einen separaten Speicherblock in unserem Puffer, und nur der Zeiger auf diesen Block wird im Vektor gespeichert.
- Wie Sie der Ausgabe entnehmen können, befindet sich "Dies ist eine längere Zeichenfolge" fast ganz am Ende des Puffers.
- Wenn wir eine weitere kurze Zeichenfolge einfügen, fällt diese in den Speicherblock des Vektors zurück
Zum Vergleich machen wir das gleiche Experiment mit
std::stringstattstd::pmr::string
sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
###w# ##########Hello World########w# ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________________________#
new 24
after longer string strings:
###w# ##########Hello World########w# ##########One Two Three###0#######################_______________________________________________________________________________________________________________________________________________________________________#
after the last string:
###w# ##########Hello World########w# ##########One Two Three###0#######################________@##w# ##########Four Five Six###_______________________________________________________________________________________________________________________________#
Diesmal nehmen die Elemente im Container weniger Platz ein, da kein Zeiger auf die memory_resource gespeichert werden muss.
Die kurzen Zeichenfolgen werden immer noch im Vektorspeicherblock gespeichert, aber jetzt ist die lange Zeichenfolge nicht in unseren Puffer gelangt. Dieses Mal wird eine lange Zeichenfolge unter Verwendung des Standardzuweisers zugewiesen und ein
Zeiger darauf wird im Vektorspeicherblock platziert. Daher sehen wir diese Zeile nicht in der Ausgabe.
Noch einmal zur Vektorexpansion:
Es wurde erwähnt, dass der Allokator den Speicher im Pool mithilfe des Operators anfordert, wenn er leer ist
new().
Tatsächlich ist dies nicht ganz richtig - Speicher wird von angefordert
memory_resourceund mit einer freien Funktion zurückgegeben.
std::pmr::memory_resource* get_default_resource()
Standardmäßig gibt diese Funktion zurück
std::pmr::new_delete_resource(), die wiederum Speicher mit einem Operator zuweist new(), aber mit einer Funktion ersetzt werden kann.
std::pmr::memory_resource* set_default_resource(std::pmr::memory_resource* r)
Schauen wir uns also ein Beispiel an, wenn
get_default_resourceein Wert von zurückgegeben wird Standard.
Es sollte beachtet werden, dass die Methoden
do_allocate()und do_deallocate()das Argument "Ausrichtung" verwendet werden, daher benötigen wir die C ++ 17-Version new()mit Ausrichtungsunterstützung:
void* lastAllocatedPtr = nullptr;
size_t lastSize = 0;
void* operator new(std::size_t size, std::align_val_t align) {
#if defined(_WIN32) || defined(__CYGWIN__)
auto ptr = _aligned_malloc(size, static_cast<std::size_t>(align));
#else
auto ptr = aligned_alloc(static_cast<std::size_t>(align), size);
#endif
if (!ptr)
throw std::bad_alloc{};
std::cout << "new: " << size << ", align: "
<< static_cast<std::size_t>(align)
<< ", ptr: " << ptr << '\n';
lastAllocatedPtr = ptr;
lastSize = size;
return ptr;
}
Kommen wir nun zum Hauptbeispiel zurück:
constexpr auto buf_size = 32;
uint16_t buffer[buf_size] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, 0);
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)*sizeof(uint16_t)};
std::pmr::vector<uint16_t> vec{ &pool };
for (int i = 1; i <= 20; ++i)
vec.push_back(i);
for (int i = 0; i < buf_size; ++i)
std::cout << buffer[i] << " ";
std::cout << std::endl;
auto* bufTemp = (uint16_t *)lastAllocatedPtr;
for (unsigned i = 0; i < lastSize; ++i)
std::cout << bufTemp[i] << " ";
Das Programm versucht, 20 Zahlen in einen Vektor einzufügen, aber da der Vektor nur wächst, benötigen wir mehr Platz als im reservierten Puffer mit 32 Einträgen.
Daher fordert der Allokator irgendwann Speicher durch an
get_default_resource, was wiederum zu einem Aufruf des Globalen führt new().
Programmausgabe:
new: 128, align: 16, ptr: 0xc73b20
1 1 2 1 2 3 4 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 132 0 0 0 0 0 0 0 144 0 0 0 65 0 0 0 16080 199 0 0 16176 199 0 0 16176 199 0 0 15344 199 0 0 15472 199 0 0 15472 199 0 0 0 0 0 0 145 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Gemessen an der Ausgabe an die Konsole reicht der zugewiesene Puffer für nur 16 Elemente aus, und wenn wir die Zahl 17 einfügen, erfolgt eine neue Zuordnung von 128 Bytes unter Verwendung des Operators
new().
In der dritten Zeile sehen wir einen Speicherblock, der mit einem Operator zugewiesen wurde
new().
Das obige Beispiel mit Bedienerüberschreibung ist
new()wahrscheinlich nicht für eine Produktlösung geeignet.
Glücklicherweise stört uns niemand, unsere eigene Schnittstellenimplementierung vorzunehmen
memory_resource.
Alles was wir brauchen ist
- geerbt von
std::pmr::memory_resource - Methoden implementieren:
do_allocate()do_deallocate()do_is_equal()
- Übergeben Sie unsere Implementierung an
memory_resourceContainer.
Das ist alles. Über den folgenden Link können Sie die Aufzeichnung des Tages der offenen Tür ansehen, in der wir detailliert über das Kursprogramm, den Lernprozess berichten und Fragen potenzieller Studenten beantworten:
Weiterlesen