Die Boost C++ Bibliotheken

Kapitel 4. Boost.Pool

Bei Boost.Pool handelt es sich um eine Bibliothek, die einige wenige Klassen zur Speicherverwaltung anbietet. Während in C++-Programmen üblicherweise mit new dynamisch Speicher angefordert wird und es von der Implementation der Standardbibliothek und vom Betriebssystem abhängt, wie Speicher im Detail zur Verfügung gestellt wird, können Sie mit Boost.Pool auf die Speicherverwaltung Einfluss nehmen. So können Sie beispielsweise die Speicherverwaltung beschleunigen, damit Ihr Programm schneller benötigten Speicher erhält.

Boost.Pool ändert weder die Funktionsweise von new noch die des Betriebssystems. Die von Boost.Pool angebotene Speicherverwaltung funktioniert nur deswegen, weil der verwaltete Speicher zuerst vom Betriebssystem angefordert wird – zum Beispiel mit new. Von außen betrachtet besitzt Ihr Programm bereits den Speicher. Innerhalb Ihres Programms benötigen Sie den Speicher jedoch noch nicht, sondern übergeben ihn bis auf Weiteres Boost.Pool zur Verwaltung.

Boost.Pool teilt den von Ihnen übergebenen Speicher in gleichgroße Segmente ein. Wann immer Sie Speicher benötigen und von Boost.Pool anfordern, greift Boost.Pool auf das nächste freie Segment zu und weist Ihnen den benötigten Speicher aus diesem Segment zu. Dieses Segment ist dann verbraucht – völlig unabhängig davon, wie viele Bytes Sie genau aus diesem Segment benötigen.

Diese Art der Einteilung wird als einfach segmentierter Speicher bezeichnet – auf Englisch simple segregated storage. Es ist die einzige Speicherverwaltung, die Boost.Pool unterstützt. Sie bietet sich vor allem dann an, wenn viele gleichgroße Objekte oft erstellt und zerstört werden müssen, da in diesem Fall der benötigte Speicher sehr schnell zur Verfügung gestellt und freigegeben werden kann.

Boost.Pool bietet mit boost::simple_segregated_storage eine Klasse an, die segmentierten Speicher erstellen und verwalten kann. Es handelt sich hierbei um eine Low-Level-Klasse, die Sie in Ihren Programmen üblicherweise nicht direkt verwenden. Sie wird im Beispiel 4.1 lediglich vorgestellt, um die Funktionsweise des einfach segmentierten Speichers zu verdeutlichen. Alle anderen Klassen von Boost.Pool basieren intern auf boost::simple_segregated_storage.

Beispiel 4.1. Einfach segmentierter Speicher mit boost::simple_segregated_storage
#include <boost/pool/simple_segregated_storage.hpp>
#include <vector>
#include <cstddef>

int main()
{
  boost::simple_segregated_storage<std::size_t> storage;
  std::vector<char> v(1024);
  storage.add_block(&v.front(), v.size(), 256);

  int *i = static_cast<int*>(storage.malloc());
  *i = 1;

  int *j = static_cast<int*>(storage.malloc_n(1, 512));
  j[10] = 2;

  storage.free(i);
  storage.free_n(j, 1, 512);
}

Um boost::simple_segregated_storage verwenden zu können, müssen Sie die Headerdatei boost/pool/simple_segregated_storage.hpp einbinden. Sie müssen außerdem einen Parameter bei der Instanziierung der Klasse angeben, da es sich bei boost::simple_segregated_storage um ein Template handelt. Im Beispiel 4.1 wird std::size_t übergeben. Der Parameter gibt den Typ für Zahlen an, die als Parameter an Methoden von boost::simple_segregated_storage übergeben werden, um zum Beispiel die Größe eines Segments zu beschreiben. Die praktische Bedeutung des Template-Parameters ist eher gering.

Interessanter sind die Methoden, die für boost::simple_segregated_storage aufgerufen werden. Zuerst wird mit add_block() ein Speicherblock bestehend aus 1024 Bytes an storage übergeben. Diese Bytes werden von v, einem Vektor vom Typ std::vector, zur Verfügung gestellt. Der dritte an add_block() übergebene Parameter gibt an, dass der Speicherblock in Segmente von 256 Bytes eingeteilt werden soll. Da der Speicherblock insgesamt 1024 Bytes groß ist, besteht der Speicher, den storage verwaltet, aus vier Segmenten.

Mit malloc() und malloc_n() wird zweimal Speicher von storage angefordert. Während malloc() einen Zeiger auf ein freies Segment zurückgibt, gibt malloc_n() einen Zeiger auf ein oder mehrere freie Segmente zurück, die zusammenhängend so viele Bytes zur Verfügung stellen wie mit malloc_n() angefordert werden. Im Beispiel 4.1 wird mit malloc_n() nach einem zusammenhängenden Speicherbereich von 512 Bytes gesucht. Durch den Aufruf von malloc_n() werden zwei Segmente verbraucht, da jedes Segment 256 Bytes groß ist. Nach dem Aufruf von malloc() und malloc_n() gibt es in storage nur mehr ein freies Segment.

Am Ende des Beispiels werden mit free() und free_n() alle Segmente an storage zurückgegeben. Nach dem Aufruf der beiden Methoden sind alle vier Segmente wieder frei und könnten mit malloc() oder malloc_n() neu angefordert werden.

Sie verwenden boost::simple_segregated_storage üblicherweise nicht direkt. Boost.Pool stellt andere Klassen zur Verfügung, die automatisch Speicher anfordern, ohne dass Sie wie im Beispiel 4.1 Speicher selbst anfordern und an boost::simple_segregated_storage übergeben müssen.

Im Beispiel 4.2 wird die Klasse boost::object_pool verwendet, die in der Headerdatei boost/pool/object_pool.hpp definiert ist.

Beispiel 4.2. Segmentierter Speicher mit boost::object_pool
#include <boost/pool/object_pool.hpp>

int main()
{
  boost::object_pool<int> pool;

  int *i = pool.malloc();
  *i = 1;

  int *j = pool.construct(2);

  pool.destroy(i);
  pool.destroy(j);
}

Ein entscheidender Unterschied zu boost::simple_segregated_storage ist, dass boost::object_pool den Typ der Objekte kennt, die Sie im Speicher ablegen möchten. So ist im Beispiel 4.2 angegeben, dass pool einen einfach segmentierten Speicher für int-Werte zur Verfügung stellen soll. Der von pool verwaltete Speicher wird daraufhin in Segmente von der Größe eines ints eingeteilt – also zum Beispiel in 4-Byte-Blöcke.

Ein weiterer entscheidender Unterschied zu boost::simple_segregated_storage ist, dass Sie boost::object_pool keinen Speicher zur Verfügung stellen müssen. boost::object_pool reserviert Speicher automatisch. So wird im Beispiel 4.2 beim Aufruf von malloc() ein Speicherblock reserviert, der 32 int-Werte umfasst. malloc() gibt einen Zeiger auf das erste dieser 32 Segmente zurück, in das genau ein int passt.

Beachten Sie, dass malloc() einen Zeiger vom Typ int* zurückgibt. Es ist kein Cast-Operator nötig, wie er im Beispiel 4.1 bei boost::simple_segregated_storage angewandt werden musste.

construct() ähnelt malloc(), initialisiert ein Objekt jedoch über einen entsprechenden Konstruktoraufruf. So zeigt im Beispiel 4.2 j auf eine int-Variable, in der 2 gespeichert ist.

Beachten Sie, dass pool den Aufruf von construct() aus dem zur Verfügung stehenden 32 int-Werte umfassenden Speicher bedienen kann. Der Aufruf von construct() führt im Beispiel 4.2 nicht dazu, dass pool Speicher vom Betriebssystem anfordert.

Die letzte im Beispiel 4.2 aufgerufene Methode destroy() gibt eine int-Variable wieder frei.

Beispiel 4.3. Speicherblockgröße von boost::object_pool verändern
#include <boost/pool/object_pool.hpp>
#include <iostream>

int main()
{
  boost::object_pool<int> pool{32, 0};
  pool.construct();
  std::cout << pool.get_next_size() << '\n';
  pool.set_next_size(8);
}

Sie können dem Konstruktor von boost::object_pool zwei Parameter übergeben. Der erste Parameter gibt die Größe des Speicherblocks an, den boost::object_pool reserviert, wenn mit malloc() oder construct() zum ersten Mal ein Segment angefordert wird. Der zweite Parameter gibt die maximale Größe des zu reservierenden Speicherblocks an.

Wenn malloc() oder construct() so oft aufgerufen werden, dass alle Segmente im Speicherblock reserviert sind, führt der nächste Aufruf einer dieser beiden Methoden dazu, dass boost::object_pool einen neuen Speicherblock reserviert. Dieser ist doppelt so groß wie der vorherige Speicherblock. Für jeden weiteren Speicherblock, den boost::object_pool benötigt, wird die Größe verdoppelt. boost::object_pool kann beliebig viele Speicherblöcke verwalten. Ihre Größen wachsen jedoch exponentiell. Über den zweiten Parameter des Konstruktors ist es möglich, das exponentielle Wachstum zu begrenzen.

Wird der Standardkonstruktor von boost::object_pool aufgerufen, ist dies gleichbedeutend mit dem Konstruktoraufruf im Beispiel 4.3. Der erste Parameter gibt an, dass der erste anzufordernde Speicherblock 32 int-Werte umfassen soll. Der zweite Parameter gibt an, dass es keine maximale Größe gibt. Wird 0 übergeben, kann boost::object_pool die Speicherblockgröße beliebig oft verdoppeln.

Der Aufruf von construct() im Beispiel 4.3 führt dazu, dass pool einen Speicherblock in der Größe von 32 int-Werten anfordert. pool kann nun bis zu 32 Aufrufe von malloc() oder construct() bedienen, ohne erneut Speicher anfordern zu müssen. Müsste erneut Speicher angefordert werden, wäre der nächste Block 64 int-Werte groß.

Über get_next_size() kann die Größe des nächsten Speicherblocks erhalten werden, über set_next_size() neu gesetzt werden. So gibt im Beispiel 4.3 get_next_size() 64 zurück. Der Aufruf von set_next_size() bewirkt, dass der nächste Speicherblock, den pool anfordern würde, keine 64, sondern lediglich 8 int-Werte umfassen würde. Über set_next_size() kann direkt Einfluss auf die Größe der Speicherblocks genommen werden. Wenn Sie lediglich eine maximale Größe vorgeben möchten, übergeben Sie diese und nicht 0 als zweiten Parameter an den Konstruktor.

Mit boost::singleton_pool bietet Boost.Pool eine Klasse an, die zwischen boost::simple_segregated_storage und boost::object_pool angesiedelt ist. Sehen Sie sich dazu Beispiel 4.4 an.

Beispiel 4.4. Segmentierter Speicher als Singleton mit boost::singleton_pool
#include <boost/pool/singleton_pool.hpp>

struct int_pool {};
typedef boost::singleton_pool<int_pool, sizeof(int)> singleton_int_pool;

int main()
{
  int *i = static_cast<int*>(singleton_int_pool::malloc());
  *i = 1;

  int *j = static_cast<int*>(singleton_int_pool::ordered_malloc(10));
  j[9] = 2;

  singleton_int_pool::release_memory();
  singleton_int_pool::purge_memory();
}

boost::singleton_pool ist in der Headerdatei boost/pool/singleton_pool.hpp definiert. Die Klasse ähnelt boost::simple_segregated_storage, weil die Segmentgröße als Template-Parameter angegeben wird und nicht der Typ der Objekte, der gespeichert werden soll. Daher geben Methoden wie malloc() und ordered_malloc() einen Zeiger vom Typ void* zurück, der explizit gecastet werden muss.

Die Klasse ähnelt jedoch auch boost::object_pool, da sie automatisch Speicher anfordert. Die Größe des nächsten Speicherblocks und eine eventuelle maximale Größe müssen jedoch als Template-Parameter angegeben werden. In dieser Hinsicht unterscheidet sich boost::singleton_pool von boost::object_pool: Sie können bei boost::singleton_pool nicht zur Laufzeit die Größe des nächsten Speicherblocks vorgeben oder ändern.

Sie können boost::singleton_pool mehrfach verwenden, um mehrere segmentierte Speicher zu verwalten. Als ersten Template-Parameter übergeben Sie an boost::singleton_pool einen Tag. Es handelt sich dabei um einen beliebigen Typ, der als Name für den entsprechenden segmentierten Speicher dient. Im Beispiel 4.4 wird die Struktur int_pool als Tag verwendet, um anzudeuten, dass singleton_int_pool Speicher für ints verwaltet. Dank dieser Tags können Sie sicherstellen, dass mehrere Singletons unterschiedliche Speicher verwalten, auch wenn der zweite Template-Parameter, der die Segmentgröße beschreibt, jeweils gleich ist. Der Tag hat keine andere Bedeutung als unterschiedliche Instanzen von boost::singleton_pool zu erstellen, die jeweils ihren eigenen Speicher verwalten.

Zur Speicherfreigabe bietet boost::singleton_pool zwei Methoden an: Mit release_memory() geben Sie alle Speicherblöcke frei, die im Moment nicht verwendet werden. Mit purge_memory() geben Sie alle Speicherblöcke frei – auch die, die im Moment verwendet werden. Der Aufruf von purge_memory() setzt einen Singleton-Pool in den Ausgangszustand zurück.

release_memory() und purge_memory() geben Speicher ans Betriebssystem zurück. Freigabe bedeutet hier nicht, dass Segmente wieder zur Verfügung stehen, weil boost::singleton_pool den intern verwalteten Speicher als verfügbar markiert. Um lediglich reservierte Segmente an boost::singleton_pool zurückzugeben, müssen Methoden wie free() oder ordered_free() aufgerufen werden.

boost::object_pool und boost::singleton_pool ermöglichen Ihnen, Speicher explizit anzufragen, wenn Sie Speicher benötigen. So müssen Sie Methoden wie malloc() oder construct() aufrufen. Mit boost::pool_allocator bietet Boost.Pool jedoch auch eine Klasse an, die Sie als Allokator an Container übergeben können. Sehen Sie sich dazu Beispiel 4.5 an.

Beispiel 4.5. Segmentierter Speicher mit boost::pool_allocator
#include <boost/pool/pool_alloc.hpp>
#include <vector>

int main()
{
  std::vector<int, boost::pool_allocator<int>> v;
  for (int i = 0; i < 1000; ++i)
    v.push_back(i);

  v.clear();
  boost::singleton_pool<boost::pool_allocator_tag, sizeof(int)>::
    purge_memory();
}

boost::pool_allocator ist in der Headerdatei boost/pool/pool_alloc.hpp definiert. Es handelt sich um einen Allokator, der Containern aus der Standardbibliothek üblicherweise als zweiter Template-Parameter übergeben werden kann. Der Allokator stellt dem Container den benötigten Speicher zur Verfügung.

boost::pool_allocator basiert intern auf boost::singleton_pool. Um beispielsweise Speicher freizugeben, müssen Sie wie im Beispiel 4.4 mit einem Tag auf boost::singleton_pool zugreifen und purge_memory() oder release_memory() aufrufen. Im Beispiel 4.5 wird als Tag boost::pool_allocator_tag verwendet. Es handelt sich dabei um einen Tag, der von Boost.Pool definiert ist und von boost::pool_allocator für den intern verwendeten boost::singleton_pool benutzt wird.

Wenn im Beispiel 4.5 zum ersten Mal push_back() aufgerufen wird, greift v auf den Allokator zu, um den notwendigen Speicher zu erhalten. Da als Allokator boost::pool_allocator zum Einsatz kommt, wird ein Speicherblock in einer Größe von 32 int-Werten reserviert und v ein Zeiger auf das erste Segment in diesem Speicherblock übergeben. Das Segment hat die Größe von einem int. Jeder weitere Aufruf von push_back() wird aus diesem ersten Speicherblock bedient, bis der Allokator feststellt, dass er einen größeren Speicherblock benötigt.

Beachten Sie, dass Sie einen Container mit clear() löschen sollten, bevor Sie Speicher wie im Beispiel 4.5 mit purge_memory() freigeben. Ein Aufruf von purge_memory() gibt zwar Speicher frei, teilt dem entsprechenden Container aber nicht mit, dass ihm der Speicher nicht mehr gehört. Ein Aufruf von release_memory() ist weniger gefährlich, da nur Speicherblöcke freigegeben werden, die im Moment nicht benötigt werden.

Neben boost::pool_allocator bietet Boost.Pool auch einen Allokator namens boost::fast_pool_allocator an. Sehen Sie sich dazu Beispiel 4.6 an.

Beispiel 4.6. Segmentierter Speicher mit boost::fast_pool_allocator
#define BOOST_POOL_NO_MT
#include <boost/pool/pool_alloc.hpp>
#include <list>

int main()
{
  typedef boost::fast_pool_allocator<int,
    boost::default_user_allocator_new_delete,
    boost::details::pool::default_mutex,
    64, 128> allocator;

  std::list<int, allocator> l;
  for (int i = 0; i < 1000; ++i)
    l.push_back(i);

  l.clear();
  boost::singleton_pool<boost::fast_pool_allocator_tag, sizeof(int)>::
    purge_memory();
}

Sie verwenden beide Allokatoren grundsätzlich auf die gleiche Art und Weise. boost::pool_allocator sollten Sie vorziehen, wenn mehrere aufeinander folgende Segmente auf einmal angefordert werden müssen. boost::fast_pool_allocator können Sie einsetzen, wenn Segmente jeweils einzeln eines nach dem anderen angefordert werden. Grob vereinfacht: Für std::vector verwenden Sie boost::pool_allocator, für std::list boost::fast_pool_allocator.

Beispiel 4.6 zeigt Ihnen, welche Template-Parameter Sie an boost::fast_pool_allocator übergeben können. boost::pool_allocator akzeptiert die gleichen Parameter.

Bei boost::default_user_allocator_new_delete handelt es sich um eine Klasse, die mit new Speicherblöcke reserviert und mit delete[] freigibt. Sie können alternativ boost::default_user_allocator_malloc_free verwenden. Diese Klasse ruft malloc() und free() auf.

boost::details::pool::default_mutex ist eine Typdefinition, die entweder auf boost::mutex oder auf boost::details::pool::null_mutex gesetzt ist. Standardmäßig wird boost::mutex verwendet, damit mehrere Threads auf einen segmentierten Speicher zugreifen können. Wenn wie im Beispiel 4.6 das Makro BOOST_POOL_NO_MT definiert ist, wird die Multithreading-Unterstützung für Boost.Pool explizit deaktiviert. Im Beispiel 4.6 wird demnach vom Allokator ein Null-Mutex verwendet.

Die letzten beiden Parameter, die im Beispiel 4.6 an boost::fast_pool_allocator übergeben werden, legen die Größe des ersten Speicherblocks sowie die maximale Größe eines Speicherblocks fest.