Allocation dynamique de mémoire – Exemple
Notez que les équations dans ce document sont affichées
à l'aide de MathJax.
Il est difficile d'enseigner convenablement et à l'aide d'exemples concrets
comment il est possible de remplacer les mécanismes standards d'allocation
et de libération dynamique de mémoire en C++,
du fait que :
- Ce problème est à la fois très abstrait (on joue en effet avec des
mécanismes comme de la mémoire brute et des purs lieux) et très concret (quand
on fait une bêtise, il y a des conséquences)
- Mettre en place des mécanismes de remplacement demande d'implémenter
plusieurs fonctions (au moins huit, soit le quatuor formé des opérateurs
new, new[], delete
et delete[], à la fois dans leur
déclinaison normale et dans leur déclinaison no-throw),
et
- Demander à des étudiants de mettre en place un environnement de test
pertinent tend à être déraisonnable – il y a trop de code à écrire qui ne soit
pas directement relié au problème pour que l'activité pédagogique soit telle
que le rapport efforts/ apprentissages soit valable
J'ai donc décidé de mettre en place une sorte de lieu pour expérimenter
avec ces mécanismes. Il y a du travail à faire pour qui souhaitera
utiliser ce qui suit, mais bon, j'ose espérer que cela rende service
malgré tout.
Idée générale
Quelques mots sur l'approche générale suivie dans ce qui suit :
- J'ai représenté la mémoire comme une classe, le type
memory_pool
- J'associe, pour mes tests, un objet représentant de la mémoire
avec une stratégie d'allocation, représentée par instances
de classes dérivées d'alloc_strategy.
J'ai implémenté une stratégie particulière mais
plutôt banale dans la classe continuous_alloc_strategy,
pour montrer comment y arriver
- Puisque je spécialise les versions globales de new, new[], delete
et delete[], j'ai implémenté
un dépôt de stratégies d'allocation sous forme du Singleton
nommé alloc_strategy.
Cette classe possède une classe interne RAII
nommée switcher<T> et capable de
remplacer une stratégie par une autre de manière temporaire (remplace la
stratégie précédente par une nouvelle à la construction, puis remet la
précédente en place à la destruction). J'utilise ce remplacement temporaire
pour réaliser des tests circonscrits sur mes stratégies d'allocation
- J'ai aussi paramétré mes tests sur la base du comportement
du code client. Deux stratégies ont été implémentées,
soit une LIFO (comme une
pile) où l'objet
le plus récemment alloué tend à être le prochain
libéré, et une FIFO (une
file) où l'objet le
plus récemment alloué sera le dernier libéré. Ceci permet de valider des
paires faites d'une stratégie d'allocation de mémoire et d'un comportement à
l'exécution pour le code client. Vous pouvez évidemment enrichir le tout, le
code étant générique
Le reste de cet article présente d'abord quelques outils généraux,
réutilisables sans problème dans des programmes complètement
différents de celui mis de l'avant ici, puis décrit les différentes
entités plus spécifiquement associées au problème
qui nous intéresse ici, soit celui de la spécialisation des outils
de gestion de la mémoire allouée dynamiquement.
Outils généraux
Les outils présentés ici sont des structures de données
réutilisables et des extensions à des en-têtes standards
qui, pour mes fins, enrichissent l'offre de service standard et facilitent mon
travail. Comme je l'indique souvent dans mes cours, (a)
quand on peut nommer un problème ou une solution, il est souvent
sage d'en faire une classe, et (b) quand on commence
à écrire du code pour des cas particuliers, il est souvent plus
simple d'écrire une solution générale propre et de voir
par la suite s'il y a réellement lieu de donner dans le particulier –
il est fréquent, dans les cas que je rencontre personnellement, que la
solution générale propre fasse en fait très bien le travail.
Parfois, on a le visage tellement collé sur un problème qu'on
le voit plus gros qu'il ne l'est réellement. Prendre un peu de recul
et généraliser, plutôt que de coûter temps et énergie,
tend à en épargner. Du moins, c'est mon petit vécu personnel.
J'ai rédigé static_stack<T,N>
et static_queue<T,N>
pour avoir en mains des structures de données simples et propres
sans avoir à recourir à de l'allocation dynamique de mémoire,
ce mécanisme étant celui que je souhaite spécialiser. Si
sizeof(T)*N est gros, pensez allouer ces objets avec std::malloc() et utiliser la version positionnelle de
new (le placement new) pour les
initialiser correctement.
Pile de capacité statique –
classe static_stack
Une static_stack<T,N> gère au maximum
N instances de T dans une structure qui ne requiert pas d'allocation
dynamique de mémoire.
Le code proposé ici est perfectible. en particulier, il serait possible d'éviter les affectations pour les remplacer
par des appels au new positionnel. Cela dit, cette implémentation nous suffira.
|
#ifndef STATIC_STACK_H
#define STATIC_STACK_H
template <class T, int N>
class static_stack {
public:
using value_type = T;
using size_type = int;
class Empty {};
class Full {};
private:
value_type elems[N];
size_type nelems {};
public:
static_stack() = default;
constexpr bool empty() const noexcept {
return !nelems;
}
bool full() const noexcept {
return nelems == capacity();
}
static constexpr size_type capacity() noexcept {
return N;
}
void push(const value_type &val) {
if (full()) throw Full{};
elems[nelems] = val;
++nelems;
}
value_type &top() {
if (empty()) throw Empty{};
return elems[nelems-1];
}
void pop() {
if (empty()) throw Empty{};
--nelems;
}
};
#endif
|
File de capacité statique –
classe static_queue
Tout comme static_stack<T,N>,
une static_queue<T,N> gère au maximum
N instances de T dans une structure qui ne requiert pas d'allocation
dynamique de mémoire.
Les remarques faites pour static_stack<T,N>
sont aussi valides dans ce cas-ci. Le problème récurrent
des files statiques est la distinction entre file vide et file pleine;
plusieurs options sont possibles, et j'ai choisi l'une d'elle, qui me
semblait plus simple à implémenter que les alternatives,
soit celle de compter le nombre d'éléments insérés.
Il n'était pas clair à mes yeux qu'en général,
par exemple, insérer un noeud sentinelle entre le point d'écriture
et le point de lecture serait preageux.
Notez que j'ai conservé les noms push(),
pop() et top() par souci de conformité
avec la pile; il aurait sans doute été plus sage de trouver
des noms plus généraux, mais bon, j'ai commencé avec
la pile et le reste fut mécanique.
|
#ifndef STATIC_QUEUE_H
#define STATIC_QUEUE_H
template <class T, int N>
class static_queue {
public:
using value_type = T;
using size_type = int;
class Empty {};
class Full {};
private:
value_type elems[N] {};
size_type write{}, read{}, fillcount{};
static constexpr size_type next(size_type n) noexcept {
return (n + 1) % N;
}
public:
static_queue() = default;
constexpr bool empty() const noexcept {
return !fillcount;
}
constexpr bool full() const noexcept {
return fillcount == capacity();
}
static constexpr size_type capacity() noexcept {
return N;
}
void push(const value_type &val) {
if (full()) throw Full{};
elems[write] = val;
write = next(write);
++fillcount;
}
value_type &top() {
if (empty()) throw Empty{};
return elems[read];
}
void pop() {
if (empty()) throw Empty{};
read = next(read);
--fillcount;
}
};
#endif
|
Extension à
<new> – fichier new_ext.h et
classe bad_alloc_of
Une allocation échouée lève traditionnellement std::bad_alloc en C++.
Le standard C++ 11
permet toutefois de lever autre chose si nous le jugeons pertinent.
J'ai simplement ajouté une nouvelle classe,
bad_alloc_of, capable d'entreposer le nombre de bytes
demandé lors de l'échec d'une allocation dynamique de mémoire.
Ainsi, il sera plus facile de produire de l'information signifiante lors
des tests dans nos programmes.
|
#ifndef NEW_EXT_H
#define NEW_EXT_H
#include <cstddef>
struct bad_alloc_of {
std::size_t value;
bad_alloc_of(std::size_t n) noexcept : value{ n } {
}
};
#endif
|
Extension à <algorithm>
– fichier algorithm_ext.h
La plupart de mes projets incluent une extension ou une autre à ce superbement
utile en-tête qu'est <algorithm>. Ce
projet ne fera pas exception.
Pour mes fins dans ce projet, deux algorithmes généraux
semblaient particulièrement pertinents, soit :
- L'algorithme circular_distance(from, to, sz)
qui retourne le nombre de pas dans
présumant que le calcul se fasse sur des entiers modulo
sz, et
- L'algorithme largest_sequence_length(debut, fin, val),
qui retourne la longueur de la plus longue séquence dans faite entièrement d'élément
de valeur val
Ces deux algorithmes seront très utiles dans la recherche d'espace
disponible dans la mémoire de nos programmes.
|
#ifndef ALGORITHM_EXT_H
#define ALGORITHM_EXT_H
#include <iterator>
#include <algorithm>
template <class T>
constexpr T circular_distance(T from, T to, T sz) {
return from <= to? to - from : sz - from + to;
}
template <class It, class T>
auto largest_sequence_length(It debut, It fin, const T &val) {
using namespace std;
using difference_type = typename
iterator_traits<It>::difference_type;
difference_type n{};
for(debut = find(debut, fin, val);
debut != fin;
debut = find(debut, fin, val)) {
auto p = find_if(debut, fin, [&val](const T &e){
return e != val;
});
n = max(n, distance(debut, p));
debut = p;
if (debut != fin) ++debut;
}
return n;
}
#endif
|
Représenter la mémoire – classe
memory_pool
Tel que mentionné plus haut, j'ai représenté la mémoire
par un objet. Cela me donne plus de latitude pour mes tests, sans changer réellement
ce qui concerne les stratégies d'allocation dynamique de mémoire
et leur validation.
Fichier memory_pool.h
Pour refléter des conditions réelles de gestion de mémoire,
j'ai défini ma classe memory_pool de
manière à ce qu'il soit possible :
- D'itérer sur de la mémoire brute (types
memory_iterator et const_memory_iterator,
qui correspondent à char* et à
const char* pour itérer sur des bytes),
et
- De gérer des questions d'alignement en mémoire (car la plupart des
ordinateurs imposent des règles pointues sur l'alignement en mémoire des
objets; une simulation ne tenant pas compte de cette réalité serait
d'une utilité restreinte)
Ici La méthode est_aligne(n) est vraie
seulement pour des n qui sont un multiple
de la taille du mot mémoire. Notez que pour gérer l'alignement
des données en mémoire, C++ 11
offre des opérateurs nommés
alignof() et alignas() que je vous
invite à explorer pour faire mieux que cet exemple académique.
|
#ifndef MEMORY_POOL_H
#define MEMORY_POOL_H
#include <cstdlib>
#include <new>
#include <vector>
#include <algorithm>
#include <cassert>
#include <iosfwd>
class NotAvailable {};
class NotTaken {};
class NotAllocated {};
class InvalidCapacity {};
class memory_pool {
public:
memory_pool(const memory_pool&) = delete;
memory_pool& operator=(const memory_pool&) = delete;
enum { TAILLE_MOT_MEMOIRE = 4 }; // +/- exact
using memory_iterator = char*;
using const_memory_iterator = const char*;
using size_type = std::size_t;
template <class T>
constexpr bool est_aligne(T n) const {
return n % TAILLE_MOT_MEMOIRE == 0;
}
|
Pour comptabiliser les espaces alloués ou non, j'utiliserai :
- Un tableau de booléens, où chaque élément
true indique une adresse prise
- La mémoire elle-même, mem_,
de taille capacity_, et
- Une liste de noeuds pris, pointée par
tete_, pour faciliter la bonne gestion de la libération
de la mémoire. Notez les fonctions creer()
et detruire() qui passent par
std::malloc() et std::free() pour
éviter la récurrence à l'infini sur new et delete, et qui utilisent le new positionnel à l'interne pour initialiser de finaliser
des instances de taken_node
La liste des noeuds utilisés repose sur une liste simplement chaînée.
J'utilise des outils primitifs pour gérer les ressources (listes
chaînées programmées manuellement, tableaux dynamiques
gérés « à bras ») parce que
je souhaite éviter d'utiliser new,
new[], delete et
delete[], cherchant à éviter les interférences
avec le code que je teste.
|
private:
size_type cap;
void *mem;
bool *pris;
struct taken_node {
taken_node *next;
char *debut;
char *fin;
taken_node(char *debut, char *fin) : debut{ debut }, fin{ fin }, next{} {
}
};
taken_node *creer(memory_iterator debut, memory_iterator fin) {
using namespace std;
void *p = malloc(sizeof(taken_node));
if (!p) throw bad_alloc{};
return new (p) taken_node(debut, fin);
}
void detruire(taken_node *p) {
assert(p);
p->~taken_node(); // ICI
free(p);
}
void clear_taken_nodes_list() {
while (tete) {
auto p = tete->next;
detruire(tete);
tete = p;
}
}
taken_node *tete;
|
Certaines opérations de base sont implémentées pour
un memory_pool. Notez que j'en ai fait un
type incopiable
mais déplaçable, sur lequel il est possibe d'itérer
à la fois pour la mémoire brute et pour les adresses allouées.
La plupart des méthodes listées ici sont banales.
Notez qu'avec les versions contemporaines du langage
C à
tout le moins, free(nullptr) est sans effet.é
|
public:
using iterator = bool*;
using const_iterator = const bool*;
memory_pool(size_type capacity) : cap{ capacity }, pris{}, tete{} {
using namespace std;
if (!est_aligne(cap)) throw InvalidCapacity{};
mem = malloc(cap);
if (!mem) throw bad_alloc{};
pris = static_cast<bool*>(malloc(cap * sizeof(bool)));
if (!pris) {
free(mem);
throw bad_alloc{};
}
}
memory_pool(memory_pool && other)
: cap{ std::exchange(other.cap, 0) },
mem{ std::exchange(other.mem, nullptr) },
tete{ std::exchange(other.tete, nullptr) },
pris{ std::exchange(other.pris, nullptr) } {
}
void swap(memory_pool &other) {
using std::swap;
swap(cap, other.cap);
swap(mem, other.mem);
swap(tete, other.tete);
swap(pris, other.pris);
}
memory_pool& operator=(memory_pool && other) {
using std::exchange;
if(this == &other) return *this;
cap = exchange(other.cap, 0);
mem = exchange(other.mem, nullptr);
tete = exchange(other.tete, nullptr);
pris = exchange(other.pris, nullptr);
return *this;
}
~memory_pool() {
using std::free;
free(mem);
free(pris);
clear_taken_nodes_list();
}
size_type size() const {
return cap;
}
memory_iterator memory_begin() noexcept {
return reinterpret_cast<memory_iterator>(mem);
}
const_memory_iterator memory_begin() const noexcept {
return reinterpret_cast<const_memory_iterator>(const_cast<const void*>(mem));
}
memory_iterator memory_end() noexcept {
return memory_begin() + size();
}
const_memory_iterator memory_end() const noexcept {
return memory_begin() + size();
}
iterator begin() noexcept {
return pris;
}
const_iterator begin() const noexcept {
return pris;
}
iterator end() noexcept {
return begin() + size();
}
const_iterator end() const noexcept {
return begin() + size();
}
|
Pour les fins de notre problème, les méthodes clés
sont :
- La méthode take(from, to),
qui alloue la plage exprimés
sous forme de positions, pas sous forme d'adresses. Notez que
from doit être aligné sur le début d'un mémoire,
et
- La méthode give_back(from), qui
libère la mémoire débutant à
from exprimé en positions
J'ai aussi défini find_end_from(from) pour
identifier la taille d'un bloc de mémoire allouée dynamiquement
et débutant à la position from,
pour que ce service (qui demande de connaître la structure de liste
simplement chaînée d'instances de taken_node)
soit implémenté à l'intérieur de la classe,
évitant ainsi de la laisser percoler vers le code client.
|
size_type find_end_from(size_type from) const {
using it_type = memory_pool::const_memory_iterator;
using std::distance;
for (auto p = tete; p; p = p->next)
if (distance(memory_begin(), const_cast<it_type>(p->debut)) == from)
return distance(memory_begin(), const_cast<it_type>(p->fin));
return size();
}
void* take(size_type from, size_type to) {
using namespace std;
assert(est_aligne(from));
assert(all_of(next(begin(), from), next(begin(), to), [](bool taken) {
return !taken;
}));
auto p = creer(next(memory_begin(), from), next(memory_begin(), to));
p->next = tete;
tete = p;
fill(next(begin(), from), next(begin(), to), true);
return p->debut;
}
void give_back(size_type from) {
using namespace std;
size_type to;
if (distance(memory_begin(), tete->debut) == from) {
to = distance(memory_begin(), tete->fin);
auto p = tete;
tete = tete->next;
detruire(p);
} else {
auto p = tete;
while (p->next && distance(memory_begin(), p->next->debut) != from)
p = p->next;
if (!p->next) throw NotAllocated{};
to = distance(memory_begin(), p->next->fin);
auto q = p->next;
p->next = p->next->next;
detruire(q);
}
assert(all_of(next(begin(), from), next(begin(), to), [](bool taken) {
return taken;
}));
fill(next(begin(), from), next(begin(), to), false);
}
bool is_free(size_type n) const {
return !pris[n];
}
size_type next_from(size_type n) const {
return ++n, n == size() ? size_type() : n;
}
void reset() noexcept;
friend void print_stats(const memory_pool&, std::ostream&);
};
#endif
|
Fichier memory_pool.cpp
L'implémentation de memory_pool a
quelques particularités intéressantes :
- La méthode reset() remet à neuf une zone
mémoire, sans faire quelque finalisation que ce soit. Cela convient à
notre environnement de test mais ne serait pas raisonnable en temps
normal
- La fonction somme_blocs_libres(), qui
utilise la nouvelle syntaxe unifiée pour les fonctions sous C++ 11
et fait la somme des tailles des blocs de mémoire libre,
et
- La fonction print_stats(), qui envoie
sur un flux des informations sur l'état de la mémoire allouée
dynamiquement. J'ai pris le calcul du taux de fragmentation sur le Wiki
portant sur ce sujet
J'ai utilisé quelques particularités intéressantes
de C++ 11
au passage, dont les déclarations auto
et les λ-expressions.
|
#include "memory_pool.h"
#include "algorithm_ext.h"
#include <algorithm>
#include <cassert>
#include <ostream>
using namespace std;
void memory_pool::reset() noexcept {
fill(begin(), end(), false);
fill(memory_begin(), memory_end(), 0);
clear_taken_nodes_list();
}
auto somme_blocs_libres(const memory_pool &pool) {
using difference_type = iterator_traits<
memory_pool::const_iterator
>::difference_type;
difference_type n{};
for (auto p = find(pool.begin(), pool.end(), false);
p != pool.end();
p = find(p, pool.end(), false))
{
auto q = find(p, pool.end(), true);
n += distance(p, q);
p = q;
}
return n;
};
void print_stats(const memory_pool &pool, ostream &os) {
//
os << "Espace occupe : ";
const auto noccupes = count(pool.begin(), pool.end(), true);
os << noccupes << '/' << pool.size() << " (taux : "
<< static_cast<double>(noccupes) / static_cast<double>(pool.size())
<< ")" << endl;
//
os << "Etat de la memoire ('*' si pris, '.' si disponible) :\n";
for(auto pris : pool)
os << (pris? '*' : '.');
os << endl;
//
auto plus_gros_bloc_libre = largest_sequence_length(pool.begin(), pool.end(), false);
os << "Plus gros espace contigu disponible : " << plus_gros_bloc_libre << " bytes" << endl;
//
os << "Taux de fragmentation : "
<< (1.0 - static_cast<double>(plus_gros_bloc_libre) /
static_cast<double>(somme_blocs_libres(pool)))
<< endl;
}
|
Fichier memory_pool_algorithms.h
Certains algorithmes s'appliquent de manière générale
à mes zones de mémoire.
Ces algorithmes sont :
- find_free_stride_from(pool, pos, n)
qui retourne la position où commence une séquence
de n bytes libres dans
pool, cette recherche étant entreprise à partir
de pos, et
- find_next_free_from(pool, pos), qui
retourne la prochaine position libre (et alignée sur un mot mémoire)
dans pool
|
#ifndef MEMORY_POOL_ALGORITHMS_H
#define MEMORY_POOL_ALGORITHMS_H
#include "memory_pool.h"
#include <cstddef>
class NoRoom {};
memory_pool::size_type find_free_stride_from(
const memory_pool&, memory_pool::size_type pos, std::size_t n
);
memory_pool::size_type find_next_free_from(
const memory_pool&, memory_pool::size_type pos
);
#endif
|
Fichier bounded_accumulator.h
Fouiller en mémoire pour trouver un espace disponible demandait
beaucoup de comptabilité à mes yeux, surtout considérant
que j'aie utilisé une recherche circulaire (une fois la fin de
la mémoire rencontrée, on poursuit au début). En
conséquence, il importe de surveiller les débordements (une
recherche cyclique revenant à son point de départ) pour
éviter la recherche à l'infini d'espaces disponibles en
mémoire.
Puisque je cherchais à découpler cette logique, lourde
à mes yeux, du reste du code, j'ai défini
bounded_accumulator, un accumulateur (entité « incrémentable »)
dont la valeur plafond est identifiée à la construction.
Ce faisant, un dépassement de capacité (ou une boucle dépassant
la taille de l'espace à explorer) lèvera une exception.
Pour mes fins, le type de l'exception éventuellement levée
est suppléé à la compilation.
|
#ifndef BOUNDED_ACCUMULATOR_H
#define BOUNDED_ACCUMULATOR_H
class HorsBornes {};
template <class T, class E = HorsBornes>
class bounded_accumulator {
public:
using value_type = T;
private:
value_type cur, ubound;
E excep;
public:
bounded_accumulator(const value_type &upper_bound, E excep = {}, const value_type &init = {})
: cur{init}, ubound{upper_bound}, excep{excep}
{
}
value_type upper_bound() const {
return ubound;
}
value_type remaining() const {
return upper_bound() - value();
}
bounded_accumulator& operator+=(const value_type &n) {
if (remaining() < n) throw excep;
cur += n;
return *this;
}
bounded_accumulator& operator++() {
if (!remaining()) throw excep;
++cur;
return *this;
}
bounded_accumulator operator++(int) {
bounded_accumulator temp{*this};
operator++();
return temp;
}
value_type value() const {
return cur;
}
bool operator==(const bounded_accumulator &ba) const {
return value() == ba.value();
}
bool operator!=(const bounded_accumulator &ba) const {
return !(*this == ba);
}
};
#endif
|
Fichier memory_pool_algorithms.cpp
Les algorithmes vont un peu de soi, et peuvent être réutilisés
et enrichis au besoin. Si vous avez des questions, n'hésitez pas
à les poser à votre chic prof.
Il est probable que vous ayez des idées pour d'autres algorithmes
de recherche dans de la mémoire disponible pour allocation dynamique.
Amusez-vous!
|
#include "memory_pool_algorithms.h"
#include "memory_pool.h"
#include "bounded_accumulator.h"
#include "new_ext.h"
#include "algorithm_ext.h"
#include <new>
#include <cstdlib>
#include <algorithm>
using namespace std;
memory_pool::size_type find_next_free_from(
const memory_pool &pool, memory_pool::size_type pos
)
{
if (pool.is_free(pos) && pool.est_aligne(pos)) return pos;
auto p = pool.next_from(pos);
while((!pool.is_free(p) && p != pos) || !pool.est_aligne(p))
p = pool.next_from(p);
if (!pool.is_free(p)) throw NoRoom();
return p;
}
memory_pool::size_type find_free_stride_from(
const memory_pool &pool, memory_pool::size_type pos, size_t n
)
{
if (pos >= pool.size()) throw bad_alloc{};
if (n >= pool.size()) throw bad_alloc{};
auto might_fit = [](const memory_pool &pool, memory_pool::size_type p, size_t n) {
return n <= pool.size() - p;
};
auto cur = find_next_free_from(pool, pos);
bounded_accumulator<memory_pool::size_type, bad_alloc_of>
nchecked (pool.size(), bad_alloc_of(n), circular_distance(pos, cur, pool.size()));
auto circular_progress = [&](memory_pool::size_type curpos) -> memory_pool::size_type {
while (!might_fit(pool, curpos, n) || !pool.est_aligne(curpos)) {
pos = pool.next_from(curpos);
curpos = find_next_free_from(pool, pos);
nchecked += circular_distance(pos, curpos, pool.size()) + 1;
}
return curpos;
};
cur = circular_progress(cur);
while (any_of(pool.begin() + cur, pool.begin() + cur + n, [](bool taken) { return taken; }))
cur = circular_progress(pool.next_from(cur));
return cur;
}
|
Encadrer les stratégies d'allocation
– classe alloc_strategy
Pour « installer » et « désinstaller »
des stratégies d'allocation, il nous faut pouvoir les entreposer en un
lieu commun, donc leur donner un type en commun. La manière typique de
faire cela est de dériver les diverses stratégies d'une stratégie
plus abstraite, soit une interface – ici, j'irai un peu plus loin et j'utiliserai
une classe abstraite, nommée alloc_strategy.
Fichier alloc_strategy.h
Pour cumuler des statistiques d'utilisation et de performance sur mes
outils de gestion de mémoire, j'ai appliqué l'idiome
NVI par lequel :
- Le parent implémente des versions non-polymorphiques et publiques
des services destinés au code client
- Le parent déclare virtuelles (ici, abstraites) et protégées des
méthodes qui seront implémentées par les enfants
- Le parent encadre, à travers les méthodes publiques concrètes,
l'utilisation des méthodes protégées. Ceci permet au parent de tirer des
statistiques d'utilisation, par exemple, ou encore d'assurer le respect
des préconditions et des postconditions des méthodes d'une interface
pour les enfants
Je cumule des statistiques d'utilisation à l'aide de la classe
interne et privée Stats. Pour mes fins,
je conserve le nombre d'appels à new,
new[], delete et
delete[], de même que le temps écoulé pour
chacun de ces appels.
|
#ifndef ALLOCSTRATEGY_H
#define ALLOCSTRATEGY_H
#include "memory_pool.h"
#include "memory_pool_algorithms.h"
#include <new>
#include <cstdlib>
#include <algorithm>
#include <iosfwd>
#include <chrono>
class alloc_strategy {
memory_pool *pool_{};
struct Stats {
enum { NEW, NEWT, DEL, DELT, N };
int nb[N];
int qte[N];
std::chrono::high_resolution_clock::duration elaps[N];
constexpr Stats() : nb{ {} }, qte{ {} }, elaps{ {} } {
}
};
Stats stats;
|
Chaque stratégie d'allocation connaît, dans mon implémentation,
la mémoire avec laquelle elle transige. J'aurais pu valider que
ce pointeur soit non-nul, mais pour une raison technique (j'ai au moins
un cas de stratégie en tête qui n'a pas besoin de ce pointeur,
et il s'agit du cas par défaut) je ne l'ai pas fait.
|
protected:
memory_pool &pool() noexcept {
return *pool_;
}
public:
alloc_strategy() = default;
constexpr alloc_strategy(memory_pool *pool) : pool_{ pool } {
}
|
Les services à implémenter pour chaque classe enfant sont
les suivants :
- allocate_one_impl(size_t) pour operator new(size_t)
- allocate_block_impl(size_t) pour operator new[](size_t)
- deallocate_one_impl(void*) pour operator delete(void*), et
- deallocate_block_impl(void*) pour operator delete[](void*)
Dans chaque cas, la méthode non-polymorphique que le code client
utilisera portera un nom plus simple (allocate_one()
plutôt qu'allocate_one_impl(),
par exemple) et encadrera l'appel de la version polymorphique par du code
de collecte de données statistiques.
Le code se répète un peu; vous pouvez le retravailler pour
réduire la redondance si le coeur vous en dit.
J'ai fait de print_stats() une fonction
amie de cette classe pour qu'elle (et elle seule) puisse fouiller dans
les statistiques accumulées. Ceci me permet de garder ces statistiques
à l'interne, cachées, et de ne pas faire d'elles une partie
de l'interface visible de la stratégie d'allocation. À
mon avis, c'est la chose à faire.0
|
void* allocate_one(std::size_t n) {
using namespace std::chrono;
auto pre = high_resolution_clock::now();
void *p = allocate_one_impl(n);
auto post = high_resolution_clock::now();
++stats.nb[Stats::NEW];
stats.qte[Stats::NEW] += n;
stats.elaps[Stats::NEW] += post - pre;
return p;
}
void* allocate_one(std::size_t n, std::nothrow_t nt) noexcept {
using namespace std::chrono;
auto pre = high_resolution_clock::now();
void *p = allocate_one_impl(n, nt);
auto post = high_resolution_clock::now();
++stats.nb[Stats::NEW];
stats.qte[Stats::NEW] += n;
stats.elaps[Stats::NEW] += post - pre;
return p;
}
void* allocate_block(std::size_t n) {
using namespace std::chrono;
auto pre = high_resolution_clock::now();
void *p = allocate_block_impl(n);
auto post = high_resolution_clock::now();
++stats.nb[Stats::NEWT];
stats.qte[Stats::NEWT] += n;
stats.elaps[Stats::NEWT] += post - pre;
return p;
}
void* allocate_block(std::size_t n, std::nothrow_t nt) noexcept {
using namespace std::chrono;
auto pre = high_resolution_clock::now();
void *p = allocate_block_impl(n, nt);
auto post = high_resolution_clock::now();
++stats.nb[Stats::NEWT];
stats.qte[Stats::NEWT] += n;
stats.elaps[Stats::NEWT] += post - pre;
return p;
}
void deallocate_one(void *p) noexcept {
using namespace std;
using namespace std::chrono;
auto pre = high_resolution_clock::now();
deallocate_one_impl(p);
auto post = high_resolution_clock::now();
++stats.nb[Stats::DEL];
stats.qte[Stats::DEL] -= pool().find_end_from(
distance(
pool().memory_begin(),
static_cast<decltype(pool().memory_begin())>(p)
)
);
stats.elaps[Stats::DEL] += post - pre;
}
void deallocate_one(void *p, std::nothrow_t nt) noexcept {
using namespace std;
using namespace std::chrono;
auto pre = high_resolution_clock::now();
deallocate_one_impl(p, nt);
auto post = high_resolution_clock::now();
++stats.nb[Stats::DEL];
stats.qte[Stats::DEL] -= pool().find_end_from(
distance(
pool().memory_begin(),
static_cast<decltype(pool().memory_begin())>(p)
)
);
stats.elaps[Stats::DEL] += post - pre;
}
void deallocate_block(void *p) noexcept {
using namespace std;
using namespace std::chrono;
auto pre = high_resolution_clock::now();
deallocate_block_impl(p);
auto post = high_resolution_clock::now();
++stats.nb[Stats::DELT];
stats.qte[Stats::DELT] -= pool().find_end_from(
distance(
pool().memory_begin(),
static_cast<decltype(pool().memory_begin())>(p)
)
);
stats.elaps[Stats::DELT] += post - pre;
}
void deallocate_block(void *p, std::nothrow_t nt) noexcept {
using namespace std;
using namespace std::chrono;
auto pre = high_resolution_clock::now();
deallocate_block_impl(p, nt);
auto post = high_resolution_clock::now();
++stats.nb[Stats::DELT];
stats.qte[Stats::DELT] -= pool().find_end_from(
distance(
pool().memory_begin(),
static_cast<decltype(pool().memory_begin())>(p)
)
);
stats.elaps[Stats::DELT] += post - pre;
}
virtual ~alloc_strategy() = default;
friend void print_stats(const alloc_strategy &, std::ostream &);
|
Les fonctions que devront implémenter les enfants sont abstraites
et respectent les signatures des opérateurs auxquelles elles correspondent.
Notez que, conformément aux usages de l'idiome
NVI, les noms les plus « jolis » ont
été conservés pour les méthodes publiques,
accessibles au code client.
|
protected:
virtual void * allocate_one_impl(std::size_t) = 0;
virtual void * allocate_one_impl(std::size_t, std::nothrow_t) noexcept = 0;
virtual void * allocate_block_impl(std::size_t) = 0;
virtual void * allocate_block_impl(std::size_t, std::nothrow_t) noexcept = 0;
virtual void deallocate_one_impl(void *) noexcept = 0;
virtual void deallocate_one_impl(void *, std::nothrow_t) noexcept = 0;
virtual void deallocate_block_impl(void *) noexcept = 0;
virtual void deallocate_block_impl(void *, std::nothrow_t) noexcept = 0;
};
#endif
|
Fichier alloc_strategy.cpp
La définition de
print_stats() est présentée à droite.
C'est une fonction somme toute banale.
|
#include "alloc_strategy.h"
#include <ostream>
#include <cassert>
#include <chrono>
using namespace std;
using namespace std::chrono;
void print_stats(const alloc_strategy &as, ostream &os) {
os << "Nb. appels a new : " << as.stats.nb[alloc_strategy::Stats::NEW] << '\n'
<< "Nb. appels a new[] : " << as.stats.nb[alloc_strategy::Stats::NEWT] << '\n'
<< "Nb. appels a delete : " << as.stats.nb[alloc_strategy::Stats::DEL] << '\n'
<< "Nb. appels a delete[] : " << as.stats.nb[alloc_strategy::Stats::DELT] << '\n'
<< "Qte memoire allouee avec new : " << as.stats.qte[alloc_strategy::Stats::NEW] << " bytes\n"
<< "Qte memoire allouee avec new[] : " << as.stats.qte[alloc_strategy::Stats::NEWT] << " bytes\n";
os << "Temps passe dans new : "
<< duration_cast<milliseconds>(as.stats.elaps[alloc_strategy::Stats::NEW]).count() << " ms.\n"
<< "Temps passe dans new[] : "
<< duration_cast<milliseconds>(as.stats.elaps[alloc_strategy::Stats::NEWT]).count() << " ms.\n"
<< "Temps passe dans delete : "
<< duration_cast<milliseconds>(as.stats.elaps[alloc_strategy::Stats::DEL]).count() << " ms.\n"
<< "Temps passe dans delete[] : "
<< duration_cast<milliseconds>(as.stats.elaps[alloc_strategy::Stats::DELT]).count() << " ms.";
}
|
Stratégie d'allocation
spécifique – classe continuous_alloc_strategy
Pour démontrer une implémentation possible d'une stratégie
d'allocation dynamique de mémoire, j'ai implémenté
continuous_alloc_strategy. L'idée est simple : une instance
de ce type se souvient de l'indice en mémoire où s'est terminée
la dernière allocation de mémoire qu'elle a prise en charge. C'est
de cet endroit que la prochaine tentative allocation sera entreprise. C'est
une stratégie un peu naïve, rien d'optimal, mais ça fonctionne.
Fichier continuous_alloc_strategy.h
Comme vous pouvez le constater, dans l'infrastructure proposée
ici, implémenter une stratégie d'allocation implique dériver
d'alloc_strategy
et implémenter les services qui y sont abstraits.
Notez que puisque les appels aux services polymorphiques sont faits dans
le code de la classe parent, et puisque c'est la visibilité (public,
protégé, privé) au point d'appel qui compte, les
implémentations des diverses méthodes dans notre classe
représentant une stratégie particulière peuvent être
privées – et elles le sont.
|
#ifndef CONTINUOUS_ALLOC_STRATEGY_H
#define CONTINUOUS_ALLOC_STRATEGY_H
#include "alloc_strategy.h"
#include "memory_pool.h"
#include <cstddef>
#include <new>
class continuous_alloc_strategy : public alloc_strategy {
memory_pool::size_type cur;
void* allocate_one_impl(std::size_t);
void* allocate_one_impl(std::size_t n, std::nothrow_t) noexcept;
void* allocate_block_impl(std::size_t n);
void* allocate_block_impl(std::size_t n, std::nothrow_t) noexcept;
void deallocate_one_impl(void *p) noexcept;
void deallocate_one_impl(void *p, std::nothrow_t) noexcept;
void deallocate_block_impl(void *p) noexcept;
void deallocate_block_impl(void *p, std::nothrow_t) noexcept;
public:
continuous_alloc_strategy(memory_pool &pool) : alloc_strategy(&pool), cur{} {
}
};
#endif
|
Fichier continuous_alloc_strategy.cpp
Les méthodes implémentant les divers services d'allocation
et de libération dynamique de mémoire sont proposées
à droite. La simplicité de la stratégie appliquée
ici fait en sorte que le code soit chaque fois plutôt simple, et
que je ne fais pas de distinction, avec cette implémentation, entre
l'allocation de tableaux et l'allocation de scalaires.
Mes méthodes ne sont pas Exception-Neutral; elles ne
laissent filtrer que des std::bad_alloc lors
d'un problème d'allocation. Cependant, les exceptions sous-jacentes
(p. ex. : NoRoom) servent à
usage interne seulement, donc c'est un moindre mal.
Notez que j'utilise à l'occasion l'opérateur statique
decltype de C++ 11,
qui est charmant et exprime le type d'une expression sans évaluer
la valeur de cette expression. C'est un outil franchement sympathique
et qui permet d'épargner un effort considérable de confection
de traits.
|
#include "continuous_alloc_strategy.h"
#include "memory_pool.h"
#include "memory_pool_algorithms.h"
#include <algorithm>
#include <iterator>
using namespace std;
void* continuous_alloc_strategy::allocate_one_impl(size_t n)
try {
auto pos = find_free_stride_from(pool(), cur, n);
auto end = pos + n;
cur = (end != pool().size())? end : memory_pool::size_type{};
return pool().take(pos, end);
} catch (NoRoom) {
throw bad_alloc{};
}
void* continuous_alloc_strategy::allocate_one_impl(size_t n, nothrow_t) noexcept
try {
return allocate_one_impl(n);
} catch (...) {
return nullptr;
}
void* continuous_alloc_strategy::allocate_block_impl(size_t n)
try {
auto pos= find_free_stride_from(pool(), cur, n);
auto end = pos + n;
cur = (end != pool().size())? end : memory_pool::size_type{};
return pool().take(pos, end);
} catch (NoRoom) {
throw bad_alloc{};
}
void* continuous_alloc_strategy::allocate_block_impl(size_t n, nothrow_t) noexcept
try {
return allocate_block_impl(n);
} catch (...) {
return nullptr;
}
void continuous_alloc_strategy::deallocate_one_impl(void *p) noexcept {
pool().give_back(
distance(
pool().memory_begin(),
static_cast<decltype(pool().memory_begin())>(p)
)
);
}
void continuous_alloc_strategy::deallocate_one_impl(void *p, nothrow_t) noexcept {
pool().give_back(
distance(
pool().memory_begin(),
static_cast<decltype(pool().memory_begin())>(p)
)
);
}
void continuous_alloc_strategy::deallocate_block_impl(void *p) noexcept {
pool().give_back(
distance(
pool().memory_begin(),
static_cast<decltype(pool().memory_begin())>(p)
)
);
}
void continuous_alloc_strategy::deallocate_block_impl(void *p, nothrow_t) noexcept {
pool().give_back(
distance(
pool().memory_begin(),
static_cast<decltype(pool().memory_begin())>(p)
)
);
}
|
Un dépôt de stratégies
d'allocation – classe alloc_strategy
Tel que mentionné précédemment, j'utilise un Singleton
nommé alloc_strategy pour gérer l'installation
d'une stratégie X remplaçant une stratégie Y
et la remise en place éventuelle de la stratégie Y lorsque
l'utilisation de la stratégie X sera terminée.
Fichier alloc_strategy_depot.h
Le dépôt de stratégies d'allocation conserve un pointeur
sur la stratégie d'allocation actuelle.
Étant donné qu'une approche RAII
est appliquée (voir la classe interne
switcher<S> pour installer la stratégie
S, plus bas), il n'est pas nécessaire de tenir à
jour une pile explicite de stratégies. C'est la beauté de
cette technique dans un cas comme celui-ci.
|
#ifndef ALLOC_STRATEGY_DEPOT_H
#define ALLOC_STRATEGY_DEPOT_H
#include "alloc_strategy.h"
#include "new_ext.h"
#include <new>
#include <cstdlib>
#include <algorithm>
class NoStrategy {};
class alloc_strategy_depot {
alloc_strategy *current{};
|
De manière cachée, j'utilise une stratégie par défaut,
qui se limite à des appels à std::malloc()
et à std::free() sur l'espace
mémoire « normal », utilisant un pointeur
nul en tant que memory_pool dans lequel puiser.
À bien y penser, j'aurais probablement dû implémenter
un memory_pool par défaut qui serait
un Null
Object plutôt que d'avoir recours à un pointeur
nul.
|
class DefaultStrategy : public alloc_strategy {
void* allocate_one_impl(std::size_t n) {
void *p = allocate_one_impl(n, std::nothrow);
if (!p) throw bad_alloc_of{n};
return p;
}
void* allocate_one_impl(std::size_t n, std::nothrow_t) noexcept {
return std::malloc(n);
}
void* allocate_block_impl(std::size_t n) {
void *p = allocate_block_impl(n, std::nothrow);
if (!p) throw bad_alloc_of{n};
return p;
}
void* allocate_block_impl(std::size_t n, std::nothrow_t) noexcept {
return std::malloc(n);
}
void deallocate_one_impl(void *p) noexcept {
std::free(p);
}
void deallocate_one_impl(void *p, std::nothrow_t) noexcept {
std::free(p);
}
void deallocate_block_impl(void *p) noexcept {
std::free(p);
}
void deallocate_block_impl(void *p, std::nothrow_t) noexcept {
std::free(p);
}
public:
DefaultStrategy() = default;
};
|
La stratégie par défaut est installée à la
construction du singleton. Par la suite, les appels aux méthodes
du singleton délèguent leur implémentation vers les
services de la stratégie d'allocation courante.
La méthode use() installe une stratégie
d'allocation et retourne la précédente. Un
switcher<S> installe une stratégie de type
S (dérivée d'alloc_strategy,
évidemment) et capture la précédente, pour la remettre
en place lors de sa finalisation.
|
static alloc_strategy_depot singleton;
DefaultStrategy default_strategy;
alloc_strategy_depot() {
// placed here to avoid depending on initialization order
current = &default_strategy;
}
public:
alloc_strategy_depot(const alloc_strategy_depot&) = delete;
alloc_strategy_depot& operator=(const alloc_strategy_depot&) = delete;
void* allocate_one(std::size_t n) {
return current->allocate_one(n);
}
void* allocate_one(std::size_t n, std::nothrow_t nt) {
return current->allocate_one(n, nt);
}
void* allocate_block(std::size_t n) {
return current->allocate_block(n);
}
void* allocate_block(std::size_t n, std::nothrow_t nt) {
return current->allocate_block(n, nt);
}
void deallocate_one(void *p) noexcept {
current->deallocate_one(p);
}
void deallocate_one(void *p, std::nothrow_t nt) noexcept {
current->deallocate_one(p, nt);
}
void deallocate_block(void *p) noexcept {
current->deallocate_block(p);
}
void deallocate_block(void *p, std::nothrow_t nt) noexcept {
current->deallocate_block(p, nt);
}
static alloc_strategy_depot &get() noexcept {
return singleton;
}
alloc_strategy* use(alloc_strategy *p) {
using std::swap;
if (!p) throw NoStrategy{};
swap(p, current);
return p;
}
template <class S>
class switcher {
alloc_strategy *old;
public:
switcher(const switcher&) = delete;
switcher& operator=(const switcher&) = delete;
switcher(S &strat) : old(alloc_strategy_depot::get().use(&strat)) {
}
~switcher() {
alloc_strategy_depot::get().use(old);
}
};
};
#endif
|
Fichier alloc_strategy_depot.cpp
Enfin, le singleton déclaré dans le fichier d'en-tête
reste à définir, ce que je fais ici dans un fichier source.
D'autres approches sont possibles, bien sûr (voir pouvez consulter
cet article
sur les singletons statiques en C++
pour en savoir plus).
|
#include "alloc_strategy_depot.h"
alloc_strategy_depot alloc_strategy_depot::singleton;
|
Programme de test
Le programme de test se présente trois blocs, soit :
- la surcharge des opérateurs eux-mêmes;
- le code de test; et
- le programme principal, qui lie le tout.
La surcharge des opérateurs est intimement liée à
l'architecture qui les sous-tend. Dans mon cas, on parle de délégation
des opérations aux services du singleton jouant le rôle de
dépôt de stratégies.
Étant donné qu'un appel à operator delete
ou à operator delete[] doit
être inoffensif sur un pointeur nul, le code présenté
à droite prend soin de traiter ce cas convenablement.
De même, un appel à new[] pour
allouer un bloc de taille nulle est légal en C++,
dans la mesure où chaque adresse retournée est distincte
des autres. Conséquemment, nous faisons en sorte que toutes les
allocations de tableaux, même sur une taille nulle, résulte
en l'allocation d'au moins un byte.
|
#include "algorithm_ext.h"
#include "memory_pool.h"
#include "alloc_strategy_depot.h"
#include "continuous_alloc_strategy.h"
#include <new>
#include <iostream>
#include <algorithm>
#include <cstdlib>
using namespace std;
void* operator new(size_t n) {
return alloc_strategy_depot::get().allocate_one(n);
}
void* operator new(size_t n, nothrow_t) {
return alloc_strategy_depot::get().allocate_one(n, nothrow);
}
void* operator new[](size_t n) {
return alloc_strategy_depot::get().allocate_block(max(static_cast<size_t>(1),n));
}
void* operator new[](size_t n, nothrow_t) {
return alloc_strategy_depot::get().allocate_block(max(static_cast<size_t>(1),n), nothrow);
}
void operator delete(void *p) {
if (!p) return;
alloc_strategy_depot::get().deallocate_one(p);
}
void operator delete(void *p, nothrow_t) {
if (!p) return;
alloc_strategy_depot::get().deallocate_one(p, nothrow);
}
void operator delete[](void *p) {
if (!p) return;
alloc_strategy_depot::get().deallocate_block(p);
}
void operator delete[](void *p, nothrow_t) {
if (!p) return;
alloc_strategy_depot::get().deallocate_block(p, nothrow);
}
|
La stratégie de test impliquait beaucoup de code répétitif,
alors je me suis permis le développement de quelques outils auxiliaires :
- Pour afficher les noms des modèles comportementaux utilisés
pour simuler un code client, j'ai utilisé des traits
non pas sur des types, mais sur des templates.
La syntaxe est particulière, mais je présume que si vous lisez ceci,
c'est le genre de truc qui peut vous amuser
- Puisque je cherche à valider le bon fonctionnement des mécaniques
d'allocation dynamique de mémoire, et puisque je souhaite évaluer leurs
performances, j'ai fait en sorte que les exceptions banales qui
pourraient survenir dû à la nature des tests mais n'auraient pas trait
au problème qui nous préoccupe soient « avalés » silencieusement. Ainsi,
j'ai écrit de petits services génériques pour ajouter un scalaire ou un
tableau à une structure comportementale et pour les enlever de ces
structures. Ceci allège beaucoup le code de test
|
#include "static_stack.h"
#include "static_queue.h"
#include <cstdlib>
#include <string_view>
template <template <class, int> class>
struct modele_allocation_traits;
template <>
struct modele_allocation_traits<static_stack> {
static constexpr auto nom() noexcept {
using std::literals;
return "Pile"sv;
}
};
template <>
struct modele_allocation_traits<static_queue> {
static constexpr auto nom() noexcept
using std::literals;
return "File"sv;
}
};
template <class T, int N, template <class, int> class M>
void push_one(M<T*,N> &st, T val) {
if (st.full()) return;
st.push(new T{val});
}
template <class T, int N, template <class, int> class M>
void push_one_tab(M<T*,N> &st, int n) {
if (st.full()) return;
st.push(new T[n]);
}
template <class T, int N, template <class, int> class M>
void pop_one(M<T*,N> &st) {
if (st.empty()) return;
delete st.top();
st.pop();
}
template <class T, int N, template <class, int> class M>
void pop_one_tab(M<T*,N> &st) {
if (st.empty()) return;
delete[] st.top();
st.pop();
}
|
Les tests en tant que tels vont comme suit :
- Je nettoie tout d'abord la mémoire, pour faire en sorte que les tests
soient tous faits sur un espace mémoire vierge
- J'initialise le générateur de nombres pseudo-aléatoires à une valeur
fixe. Ceci peut sembler étrange, mais l'idée ici est non pas d'avoir des
nombres imprévisibles mais bien d'avoir un comportement « aléatoire » qui
soit identique pour tous les tests, question de comparer des pommes avec
des pommes et des oranges avec des oranges
- J'instancie ensuite une stratégie applicable sur une mémoire
particulière, et je l'installe de manière
RAII
(pour qu'elle soit automatiquement désinstallée en fin de
parcours)
- Pour le reste, de manière équiprobable, j'alloue ou je libère
dynamiquement de la mémoire, que ce soit pour des scalaires ou pour des
tableaux de taille variable. J'ai choisi d'utiliser des types primitifs
pour que la finalisation ne soit pas un problème (pour que « nettoyer »
la mémoire n'implique que la remplir de zéros, pas identifier les
destructeurs les plus appropriés; nous testons une mécanique
d'allocation, après tout)
- Le nombre de tests utilisé ici est arbitraire. À partir
de quelques tests maison, j'ai pu constater qu'une exception résultant
d'une fragmentation excessive de la mémoire disponible survenait
pre ce seuil dans chaque cas. Un for ever (exprimé
typiquement sur la forme for(;;)) aurait
aussi fait le boulot
|
template <class S, template <class, int> class M>
void test_strategy(memory_pool &pool, std::ostream &os) {
os << "\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n\n"
<< "Test d'allocation dynamique memoire.\nModele comportemental : "
<< modele_allocation_traits<M>::nom() << endl;
pool.reset();
srand(0); // volontaire
S strategy{pool};
alloc_strategy_depot::switcher<S> asw(strategy);
int i;
try {
M<int*, 100> ints;
M<short*, 100> shorts;
M<char*, 100> chars;
M<int*, 100> tints;
M<short*, 100> tshorts;
M<char*, 100> tchars;
for (i = 0; i < 300; ++i) {
if (rand() % 2)
switch(rand() % 6) {
case 0:
push_one(ints, 3);
break;
case 1:
push_one(shorts, short(-2));
break;
case 2:
push_one(chars, 'A');
break;
case 3:
push_one_tab(tints, rand() % 20 + 10);
break;
case 4:
push_one_tab(tshorts, rand() % 20 + 10);
break;
case 5:
push_one_tab(tchars, rand() % 20 + 10);
break;
}
else
switch(rand() % 6) {
case 0:
pop_one(ints);
break;
case 1:
pop_one(shorts);
break;
case 2:
pop_one(chars);
break;
case 3:
pop_one_tab(tints);
break;
case 4:
pop_one_tab(tshorts);
break;
case 5:
pop_one_tab(tchars);
break;
}
}
} catch (bad_alloc_of &bao) {
os << "Echec d'allocation, iteration " << i
<< " pour une tentative d'allocation de "
<< bao.value << " bytes\n";
} catch (...) {
os << "Echec d'allocation, iteration " << i
<< " pour une tentative d'allocation de ??? bytes\n";
}
print_stats(pool, os);
print_stats(strategy, os);
os << endl;
}
|
Enfin, le programme principal réalise les tests. J'ai utilisé
chaque fois le même espace mémoire, d'une petite capacité
(1000 bytes) pour que l'affichage
soit plus utile pédagogiquement, mais je vous invite à modifier
ce paramètre pour expérimenter.
|
int main() {
enum { CAPACITE = 1000 };
memory_pool pool(CAPACITE);
test_strategy<continuous_alloc_strategy, static_stack>(pool, std::cout);
test_strategy<continuous_alloc_strategy, static_queue>(pool, std::cout);
}
|
Les résultats affichés à la sortie standard sur mon petit
ordinateur portatif vont comme suit. En mode Debug :
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Test d'allocation dynamique memoire.
Modele comportemental : Pile
Echec d'allocation, iteration 171 pour une tentative d'allocation de 92 bytes
Espace occupe : 609/1000 (taux : 0.609)
Etat de la memoire ('*' si pris, '.' si disponible) :
........................******************************************..**..********
********************************************************************************
************................**..............................**..****************
**********......................**......**************************..**..........
....................*...........................................................
*****.......................................................................****
*********...********************************************************************
************************....................................................****
************************************************************************........
................................**********************************************..
**..**..******************************************..**********************..****
******************************************************......********************
**************************..............
Plus gros espace contigu disponible : 71 bytes
Taux de fragmentation : 0.818414
Nb. appels a new : 34
Nb. appels a new[] : 46
Nb. appels a delete : 24
Nb. appels a delete[] : 32
Qte memoire allouee avec new : 79 bytes
Qte memoire allouee avec new[] : 2041 bytes
Temps passe dans new : 4.02495 ms.
Temps passe dans new[] : 44.3591 ms.
Temps passe dans delete : 1.52107 ms.
Temps passe dans delete[] : 9.10346 ms.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Test d'allocation dynamique memoire.
Modele comportemental : File
Echec d'allocation, iteration 225 pour une tentative d'allocation de 108 bytes
Espace occupe : 690/1000 (taux : 0.69)
Etat de la memoire ('*' si pris, '.' si disponible) :
................................................********************************
******************......................****************************************
****************........................****************************************
********************************************************************************
****************************************************............................
........................******************************************************..
**********************************************..********************************
**********************..********************************************************
***************************************************************.**..............
**..............*...******..**..........**************************..............
........**......**************************..**..**..****************************
********************................................******..................****
**********..............................
Plus gros espace contigu disponible : 52 bytes
Taux de fragmentation : 0.832258
Nb. appels a new : 46
Nb. appels a new[] : 60
Nb. appels a delete : 33
Nb. appels a delete[] : 43
Qte memoire allouee avec new : 107 bytes
Qte memoire allouee avec new[] : 2624 bytes
Temps passe dans new : 5.40474 ms.
Temps passe dans new[] : 41.639 ms.
Temps passe dans delete : 2.02421 ms.
Temps passe dans delete[] : 11.6965 ms.
En mode Release, maintenant :
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Test d'allocation dynamique memoire.
Modele comportemental : Pile
Echec d'allocation, iteration 171 pour une tentative d'allocation de 92 bytes
Espace occupe : 609/1000 (taux : 0.609)
Etat de la memoire ('*' si pris, '.' si disponible) :
........................******************************************..**..********
********************************************************************************
************................**..............................**..****************
**********......................**......**************************..**..........
....................*...........................................................
*****.......................................................................****
*********...********************************************************************
************************....................................................****
************************************************************************........
................................**********************************************..
**..**..******************************************..**********************..****
******************************************************......********************
**************************..............
Plus gros espace contigu disponible : 71 bytes
Taux de fragmentation : 0.818414
Nb. appels a new : 34
Nb. appels a new[] : 46
Nb. appels a delete : 24
Nb. appels a delete[] : 32
Qte memoire allouee avec new : 79 bytes
Qte memoire allouee avec new[] : 2041 bytes
Temps passe dans new : 0.0870921 ms.
Temps passe dans new[] : 0.215949 ms.
Temps passe dans delete : 0.0570603 ms.
Temps passe dans delete[] : 0.0901651 ms.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Test d'allocation dynamique memoire.
Modele comportemental : File
Echec d'allocation, iteration 225 pour une tentative d'allocation de 108 bytes
Espace occupe : 690/1000 (taux : 0.69)
Etat de la memoire ('*' si pris, '.' si disponible) :
................................................********************************
******************......................****************************************
****************........................****************************************
********************************************************************************
****************************************************............................
........................******************************************************..
**********************************************..********************************
**********************..********************************************************
***************************************************************.**..............
**..............*...******..**..........**************************..............
........**......**************************..**..**..****************************
********************................................******..................****
**********..............................
Plus gros espace contigu disponible : 52 bytes
Taux de fragmentation : 0.832258
Nb. appels a new : 46
Nb. appels a new[] : 60
Nb. appels a delete : 33
Nb. appels a delete[] : 43
Qte memoire allouee avec new : 107 bytes
Qte memoire allouee avec new[] : 2624 bytes
Temps passe dans new : 0.175232 ms.
Temps passe dans new[] : 0.309816 ms.
Temps passe dans delete : 0.0771746 ms.
Temps passe dans delete[] : 0.24633 ms.
Et voilà!