Pourquoi une programmation parallèle efficace doit inclure une allocation de mémoire évolutive

Processeur multicœur? Oui.

Ecrire un programme à exécuter en parallèle? Oui.

Avez-vous pensé à utiliser un allocateur de mémoire évolutif? Non? Alors lisez la suite…

D'après mon expérience, s'assurer que «l'allocation de mémoire» pour un programme est prête pour le parallélisme est un élément souvent négligé du bon fonctionnement d'un programme parallèle. Je peux vous montrer un moyen incroyablement simple de voir si c'est un problème pour un programme compilé (C, C ++, Fortran, etc.) - ainsi que comment le résoudre.

Une partie essentielle de tout programme parallèle est l'allocation de mémoire évolutive, qui inclut l'utilisation d'  appels nouveaux  et explicites à  malloc, calloc ou realloc . Les options incluent TBBmalloc (Intel Threading Building Blocks), jemalloc et tcmalloc. TBBmalloc a une nouvelle fonctionnalité de «proxy» qui permet d'essayer facilement en moins de 5 minutes sur n'importe quel programme compilé.

Les avantages en termes de performances de l'utilisation d'un allocateur de mémoire évolutif sont importants. TBBmalloc a été parmi les premiers allocateurs de mémoire évolutifs largement utilisés, en grande partie parce qu'il est fourni gratuitement avec TBB pour aider à souligner l'importance d'inclure des considérations d'allocation de mémoire dans tout programme parallèle. Il reste extrêmement populaire aujourd'hui et reste l'un des meilleurs allocateurs de mémoire évolutifs disponibles.

Une solution simple sans aucun changement de code

En utilisant les méthodes proxy, nous pouvons remplacer globalement new / delete et malloc / calloc / realloc / free / etc. routines avec une technique de remplacement d'interface mémoire dynamique. Cette manière automatique de remplacer les fonctions par défaut pour l'allocation dynamique de mémoire est de loin la manière la plus courante d'utiliser TBBmalloc. C'est facile et suffisant pour la plupart des programmes.

Les détails du mécanisme utilisé sur chaque système d'exploitation varient un peu, mais l'effet net est le même partout.

Nous commençons notre essai de 5 minutes en téléchargeant et en installant Threading Building Blocks (gratuit sur //threadingbuildingblocks.org; il est également inclus dans les produits Intel Parallel Studio).

Utiliser un proxy sous Linux

Sous Linux, nous pouvons faire le remplacement soit en chargeant la bibliothèque proxy au moment du chargement du programme en utilisant la variable d' environnement LD_PRELOAD (sans changer le fichier exécutable), soit en liant le fichier exécutable principal avec la bibliothèque proxy ( -ltbbmalloc_proxy ). Le chargeur de programme Linux doit être capable de trouver la bibliothèque proxy et la bibliothèque d'allocateur de mémoire évolutive au moment du chargement du programme. Pour cela, nous pouvons inclure le répertoire contenant les bibliothèques dans la variable d'environnement LD_LIBRARY_PATH ou l'ajouter à /etc/ld.so.conf .

Essayez comme suit:

time ./a.out (ou quel que soit le nom de notre programme)

export LD_PRELOAD = libtbbmalloc_proxy.so.2

time ./a.out (ou quel que soit le nom de notre programme)

Utiliser un proxy sur macOS

Sous macOS, nous pouvons effectuer le remplacement soit en chargeant la bibliothèque proxy au moment du chargement du programme à l'aide de la variable d'environnement DYLD_INSERT_LIBRARIES (sans changer le fichier exécutable), soit en liant le fichier exécutable principal avec la bibliothèque proxy ( -ltbbmalloc_proxy ). Le chargeur de programme macOS doit pouvoir trouver la bibliothèque proxy et la bibliothèque d'allocation de mémoire évolutive au moment du chargement du programme. Pour cela, nous pouvons inclure le répertoire contenant les bibliothèques dans la variable d'environnement DYLD_LIBRARY_PATH .

Essayez comme suit:

time ./a.out (ou quel que soit le nom de notre programme)

export DYLD_INSERT_LIBRARIES = $ TBBROOT / lib / libtbbmalloc_proxy.dylib

time ./a.out (ou quel que soit le nom de notre programme)

Utiliser un proxy sous Windows

Sous Windows, nous devons modifier notre exécutable. Nous pouvons soit forcer le chargement de la bibliothèque proxy en ajoutant un #include "tbb / tbbmalloc_proxy.h" dans notre code source, soit en utilisant certaines options de l'éditeur de liens lors de la construction de l'exécutable:

Pour win32:

            tbbmalloc_proxy.lib / INCLUDE: "___ TBB_malloc_proxy"

Pour win64:

            tbbmalloc_proxy.lib / INCLUDE: "__ TBB_malloc_proxy"

Le chargeur de programme Windows doit pouvoir trouver la bibliothèque proxy et la bibliothèque d'allocateur de mémoire évolutive au moment du chargement du programme. Pour cela, nous pouvons inclure le répertoire contenant les bibliothèques dans la variable d'environnement PATH . Essayez-le en utilisant Visual Studio «Performance Profiler» pour chronométrer le programme avec et sans l'option d'inclusion ou de lien.

Tester l'utilisation de notre bibliothèque proxy avec un petit programme

Je vous encourage à essayer avec votre propre programme comme décrit ci-dessus. Exécutez avec et sans le proxy et découvrez les avantages de votre application. Les applications avec beaucoup de parallélisme et beaucoup d'allocations de mémoire voient souvent des augmentations de 10 à 20% (j'ai également vu une augmentation de 400% une fois), tandis que les programmes avec peu de parallélisme ou peu d'allocations peuvent ne voir aucun effet. Les tests rapides, décrits précédemment, avec la bibliothèque proxy vous indiqueront dans quelle catégorie appartient votre application.

J'ai également écrit un programme court pour illustrer les effets ainsi que pour fournir un moyen simple de vérifier que les éléments sont installés et fonctionnent comme prévu. Nous pouvons essayer la bibliothèque proxy avec un programme simple:

#comprendre

#include "tbb / tbb.h"

en utilisant l'espace de noms tbb;

const int N = 1000000;

int main() {

double * a [N];

parallel_for (0, N-1, [&] (int i) {a [i] = nouveau double;});

parallel_for (0, N-1, [&] (int i) {supprimer un [i];});

return 0;

}

Mon exemple de programme utilise beaucoup d'espace dans la pile, donc « ulimit –s unlimited » (Linux / macOS) ou « / STACK: 10000000 » (Visual Studio: Propriétés> Propriétés de configuration> Éditeur de liens > Système> Taille de la réserve de pile) sera important pour éviter les accidents immédiats.

Après la compilation, voici les différentes façons dont j'ai exécuté mon petit programme pour voir la vitesse avec et sans la bibliothèque proxy.

En exécutant et chronométrant tbb_mem.cpp sur une machine Linux virtuelle à quatre cœurs , j'ai vu ce qui suit:

% time ./tbb_mem 

réel 0m0.160s

utilisateur 0m0.072s

sys 0m0.048s

%

% exportLD_PRELOAD = $ TBBROOT / lib / libtbbmalloc_proxy.dylib

%

% time ./tbb_mem 

réel 0m0.043s

utilisateur 0m0.048s

sys 0m0.028s

Exécution et minutage de tbb_mem.cpp sur un iMac quadcore (macOS), j'ai vu ce qui suit:

% time ./tbb_mem

réel 0m0.046s

utilisateur 0m0.078s

sys 0m0.053s

%

% export DYLD_INSERT_LIBRARIES = $ TBBROOT / lib / libtbbmalloc_proxy.dylib

%

% time ./tbb_mem 

réel 0m0.019s

utilisateur 0m0.032s

sys 0m0.009s

Sous Windows, en utilisant Visual Studio «Performance Profiler» sur un processeur Intel NUC (Core i7) quadricœur, j'ai vu des temps de 94 ms sans le profileur de mémoire évolutif et de 50 ms avec (en ajoutant #include «tbb / tbbmalloc_proxy.h» dans l'exemple de programme) .

Considérations relatives à la compilation

Personnellement, je n'ai pas eu de problème avec les compilateurs faisant des «optimisations malloc», mais techniquement, je suggérerais que lors de la compilation avec des programmes, ces «optimisations malloc» du compilateur devraient être désactivées. Il peut être judicieux de consulter la documentation du compilateur de votre compilateur préféré. Par exemple, avec les compilateurs Intel ou gcc, il est préférable de passer les indicateurs suivants:

-fno-builtin-malloc (sous Windows: / Qfno-builtin-malloc)

-fno-builtin-calloc (sous Windows: / Qfno-builtin-calloc)

-fno-builtin-realloc (sous Windows: / Qfno-builtin-realloc)

-fno-builtin-free (sous Windows: / Qfno-builtin-free)

Ne pas utiliser ces indicateurs peut ne pas poser de problème, mais ce n'est pas une mauvaise idée d'être prudent.

Sommaire

L'utilisation d'un allocateur de mémoire évolutif est un élément essentiel de tout programme parallèle. J'ai montré que TBBmalloc peut être facilement injecté sans nécessiter de changement de code (bien que l'ajout d'un «include» sur Windows soit ma solution Windows préférée). Vous pourriez voir une belle accélération avec seulement 5 minutes de travail, et vous pouvez l'appliquer facilement à plusieurs applications. Sur Linux et macOS, vous pourrez peut-être même accélérer les programmes sans avoir le code source!

Cliquez ici pour télécharger votre essai gratuit de 30 jours d'Intel Parallel Studio XE.