Softwareentwicklung: Spezielle Allokatoren mit C++17
Nach der Theorie der polymorphen Allokatoren in C++17 geht es diesmal um deren praktische Anwendung.
- Rainer Grimm
In meinem letzten Artikel "Softwareentwicklung: Polymorphe Allokatoren mit C++17" habe ich die Theorie der polymorphen Allokatoren in C++17 vorgestellt. Heute werde ich die Theorie anwenden.
Bevor ich fortfahre, hier noch einmal die wichtigsten Teile meines letzten Artikels "Softwareentwicklung: Polymorphe Allokatoren mit C++17".
Eine kurze Erinnerung
Das folgende Programm verwendet polymorphe Allokatoren:
// polymorphicAllocator.cpp
#include <array>
#include <cstddef>
#include <memory_resource>
#include <vector>
int main() {
std::array<std::byte, 200> buf1; // (1)
std::pmr::monotonic_buffer_resource pool1{buf1.data(),
buf1.size()};
std::pmr::vector<int> myVec1{&pool1}; // (3)
for (int i = 0; i < 5; ++i) {
myVec1.push_back(i);
}
char buf2[200] = {}; // (2)
std::pmr::monotonic_buffer_resource pool2{std::data(buf2),
std::size(buf2)};
std::pmr::vector<int> myVec2{&pool2};
for (int i = 0; i < 200; ++i) {
myVec2.push_back(i);
}
}
Jetzt möchte ich mich auf myVec2
konzentrieren. 200 int
s werden auf den std::pmr::vector<int>
geschoben. Diese 200 int
s passen nicht in ein char buf[200]
und deshalb springt std::pmr::new_delete_resource()
als sogenannter Upstream-Allokator ein und ruft das globale new
für die restlichen Elemente auf. Den Upstream-Allokator werde ich nun instrumentalisieren.
Ein Tracking-Allokator
Das folgende Programm basiert auf dem vorherigen, verwendet einen Tracking-Allokator und macht die dynamische Speicherzuweisung und -freigabe sichtbar.
// trackAllocator.cpp
#include <array>
#include <cstdlib>
#include <format>
#include <iostream>
#include <memory_resource>
#include <vector>
class TrackAllocator : public std::pmr::memory_resource {
void* do_allocate(std::size_t bytes,
std::size_t alignment) override {
void* p = std::pmr::new_delete_resource()->
allocate(bytes, alignment);
std::cout <<
std::format(" do_allocate: {:6} bytes at {}\n",
bytes, p);
return p;
}
void do_deallocate(void* p,
std::size_t bytes,
std::size_t alignment) override {
std::cout <<
std::format(" do_deallocate: {:4} bytes at {}\n",
bytes, p);
return
std::pmr::new_delete_resource()->
deallocate(p, bytes, alignment);
}
bool do_is_equal(const std::pmr::memory_resource& other)
const noexcept override {
return std::pmr::new_delete_resource()->is_equal(other);
}
};
int main() {
std::cout << '\n';
TrackAllocator trackAllocator; // (1)
std::pmr::set_default_resource(&trackAllocator); // (2)
std::cout << "myVec1\n";
std::array<std::byte, 200> buf1;
std::pmr::monotonic_buffer_resource pool1{buf1.data(),
buf1.size()};
std::pmr::vector<int> myVec1{&pool1}; // (3)
for (int i = 0; i < 5; ++i) {
myVec1.push_back(i);
}
std::cout << "myVec2\n";
char buf2[200] = {};
std::pmr::monotonic_buffer_resource pool2{std::data(buf2),
std::size(buf2)};
std::pmr::vector<int> myVec2{&pool2}; // (4)
for (int i = 0; i < 200; ++i) {
myVec2.push_back(i);
}
std::cout << '\n';
}
TrackAllocator
ist ein Allokator zum Nachverfolgen der Speicherallokation und deren Freigabe. Er leitet sich von der Schnittstellenklasse std::pmr::memory_resource
ab, von der sich alle Speicherressourcen ableiten. TrackAllocator
definiert die drei erforderlichen Mitgliedsfunktionen do_allocate
, do_deallocate
und do_is_equal
. Jeder Aufruf leitet diesen an den Aufruf von std::pmr::new_delete_resource
weiter. std:pmr::new_delete_resource
ist die Standard-Speicherressource und ruft das globale new
und delete
auf. Die Aufgabe der Mitgliedsfunktion do_is_equal
ist es, zu prüfen, ob die beiden Speicherressourcen gleich sind. Der interessante Punkt an dem Beispiel ist, dass ich die Zuweisung und Freigabe des Speichers in den Mitgliedsfunktionen do_allocate
und do_deallocate
visualisiere.
Die mit C++20 eingeführten Concepts haben zusammen mit der Ranges-Bibliothek, Modulen und Coroutinen das Erstellen von modernen C++- Anwendungen neu definiert. Vom 7. bis zum 9. November 2023 bringt Sie Rainer Grimm in seinem Intensiv-Workshop C++20: die neuen Konzepte umfassend erklärt auf Stand und geht auf die vielen nützlichen Funktionen ein, die C++20 mitbringt.
Ich instanziiere den TrackAllocator
(Zeile 1) und mache ihn zur Standardressource (2). myVec1
(3) und myVec2
(4) verwenden ihn als Upstream-Allokator. Dieser Allokator springt ein, wenn der primäre Allokator konsumiert ist. Dieser Fallback ist für myVec1
nicht notwendig, für myVec2
aber schon.
Diese Ausgabe zeigt die dynamische Zuweisung und Freigabe von myVec2
, bis std::pmr::vector<int>
alle int
s aufnehmen kann.
Ein Upstream-Allokator lässt sich auch an einen bestimmten Container binden.
Ein nicht-allokierender Allokator
std::pmr::null_resource_allocator
ist ein besonderer Allokator. Sie für die Allokation zu verwenden, führt zu einer std::bad_alloc
-Ausnahme. Diese Speicherressource stellt sicher, dass man nicht willkürlich Speicher auf dem Heap alloziert:
// nullMemoryResource.cpp
#include <array>
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>
int main() {
std::cout << '\n';
std::array<std::byte, 2000> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(),
buf.size(), // (1)
std::pmr::null_memory_resource()};
std::pmr::vector<std::pmr::string> myVec{&pool}; // (2)
try {
for (int i = 0; i < 100; ++i) { // (3)
std::cerr << i << " ";
myVec.emplace_back("A short string");
}
}
catch (const std::bad_alloc& e) { // (4)
std::cerr << '\n' << e.what() << '\n';
}
std::cout << '\n';
}
Zuerst weise ich Speicher auf dem Stack zu und initialisiere std::pmr::monotonic_buffer_resource
damit. Dann verwende ich diese Speicherressource und std::pmr::null_memory_resource
als Upstream-Allokator (1). In (2) erstelle ich einen std::pmr::vector<std::pmr::string>
. Da ich einen std::pmr::string
verwende, nutzt der String auch die Speicherressource und ihren Upstream-Allokator. Wenn ich std::pmr::vector<std::string>
verwende, nutzt std::string
die globalen Allokatoren new
und delete
. Schließlich erstelle ich in (3) 100 Strings und fange in (4) eine std::bad_alloc
-Ausnahme ab. Ich habe bekommen, was ich verdient habe: 100 Strings passen nicht in einen std::array<std::byte, 2000>
-Puffer. Nach 17 Strings ist der Puffer aufgebraucht.
Wie geht's weiter?
Der std::pmr::monotonic_buffer
besitzt hervorragende Eigenschaften. Er ist ziemlich schnell und gibt keinen Speicher frei. Die Zahlen dazu liefert mein nächster Artikel.
(rme)