Dans cet article, nous allons vous présenter en détails un des nouveaux systèmes de Wwise 2024.1, un nouvel allocateur mémoire par défaut pour Wwise appelé AkMemoryArena. En comparaison avec les versions précédentes de Wwise, ce nouvel allocateur mémoire peut apporter des améliorations significatives à l'utilisation des ressources mémoire de Wwise, mais en étudiant et en configurant adéquatement le système de mémoire, il est possible d'obtenir des optimisations encore plus importantes pour de nombreux projets.
Survol concernant la réservation et la fragmentation de la mémoire
Wwise 2019.2 incluait le support d'un nouvel allocateur mémoire par défaut. Les points majeurs de cet allocateur sont les suivants :
- Allocations basées sur des blocs à toutes les classes de taille, pour atténuer la fragmentation interne de la mémoire.
- Mise en cache des threads, permettant aux threads individuels d'avoir peu ou pas de contention lors de l'allocation et de la libération de la mémoire.
- Augmentation des ressources mémoire à la demande, selon les besoins, ce qui supprime la nécessité d'avoir des pools de mémoire de taille prédéfinie.
Ce nouvel allocateur mémoire a fourni d'excellentes performances, et la capacité de mapper la mémoire à la demande a grandement amélioré la flexibilité de Wwise.
Cependant, malgré la présence des ces améliorations, nous avons constaté qu'au fur et à mesure que plus de développeurs l'utilisaient, certaines lacunes ont été mises en lumière concernant l'utilisation globale de la mémoire. Trois problèmes principaux ont été relevés :
- Le schéma d'allocation basé sur les blocs signifiait que chaque allocation avait tendance à être plus importante que nécessaire et, en moyenne, la mémoire utilisée totale (« Memory used ») par de nombreux titres augmentait d'environ 10 à 15 %.
- Le comportement de mise en cache des threads entraînait des augmentations de la mémoire réservée (« Memory Reserved ») au-delà de ce qui était attendu, simplement parce que les allocations de mémoire se produisaient sur un plus grand nombre de threads. Dans un cas particulièrement problématique, nous avons vu des dizaines de threads gérés par l'application instanciant des tas uniques, qui n'étaient souvent pratiquement jamais utilisés ni libérés.
- L'allocateur demandait de nouvelles pages de mémoire dans de grandes zones (spans) à la fois, mais il s'attendait également à ce que des sous-ensembles de ces zones puissent être désaffectés au fil du temps – laissant ces parties inutilisées – avant d'être complètement libérées ensemble.
Bien que certains de ces problèmes soient partagés avec d'autres allocateurs mémoire avec cache de threads, d'autres sont survenus spécifiquement en raison de certaines conditions d'opération de Wwise : c'est-à-dire du fait que le moteur audio de Wwise soit une bibliothèque d'intergiciel (middleware). Le fait d'être un intergiciel signifie que Wwise se trouve au milieu de la pile logicielle et, après tout, ne peut donc pas interagir avec les aspects les plus bas de la plateforme. Cela inclut des choses telles que le contrôle direct de chaque thread utilisé pour l'exécution, ou l'interaction directe avec les systèmes de mémoire virtuelle pour le mappage et le démappage de la mémoire. Par exemple, nous avons constaté que la plupart des intégrations de moteurs de jeux avec Wwise ne pouvaient pas supporter le concept de démappage partiel de la mémoire, ce qui a souvent eu pour conséquence de laisser de grandes quantités de mémoire réservées mais inutilisées.
Au fil des ans, diverses mesures ont été introduites pour garder sous contrôle la quantité de mémoire réservée, mais elles ont eu tendance à augmenter le coût CPU de l'utilisation de l'allocateur, ce qui a limité une grande partie des avantages initiaux. Même en dépit de ces efforts, il y avait des décisions de conception fondamentales avec l'allocateur mémoire que nous ne pouvions raisonnablement pas contourner afin d'obtenir la meilleure utilisation possible des ressources pour le moteur audio de Wwise.
Avant de poursuivre, il convient de noter qu'il existe de nombreux cas d'utilisation où les allocateurs mémoire avec cache de threads peuvent donner d'excellents résultats, comme dans les applications de bureau ou de serveur, où de grandes quantités de mémoire sont disponibles. Par exemple, le projet LLVM a récemment déplacé son allocateur mémoire par défaut pour sa chaîne d'outils sur Windows vers rpmalloc et a constaté des améliorations de performance substantielles dans le processus. Nous avons simplement conclu qu'essayer d'appliquer cette stratégie n'était pas la meilleure solution pour Wwise, en raison des nombreuses exigences uniques de notre logiciel, qui ne sont pas universellement partagées par d'autres applications.
Ceci étant dit, discutons maintenant de l'AkMemoryArena, notre nouveau système d'allocation mémoire qui, selon nous, répond mieux aux exigences de Wwise.
Aperçu de l'AkMemoryArena
Dans la version 2024.1, nous avons intégré AkMemoryArena comme nouvel allocateur mémoire pour Wwise. Contrairement aux allocateurs mémoire offerts par Wwise au fil des ans, qui étaient tous des solutions génériques prêtes à l'emploi correspondant à notre utilisation, c'est la première fois que nous avons entièrement conçu un allocateur mémoire complet à partir de la base. Cela nous a permis de cibler toutes les exigences de Wwise dès le départ.
L'AkMemoryArena comprend les caractéristiques suivantes :
Possibilité de mapper et de démapper des zones (spans) de mémoire de manière dynamique
Nous conservons la possibilité de mapper de nouvelles zones de mémoire lorsque cela est nécessaire, ainsi que de les démapper lorsque toutes les allocations qui les composent ont été libérées. Cela signifie que nous n'exigeons pas strictement que les pools de mémoire soient prédimensionnés avec une limite stricte. D'après notre expérience, le fait de ne pas avoir de limite prédéterminée pour la mémoire offre beaucoup plus de flexibilité et de stabilité à la plupart des développeurs.
Bonnes performances en matière de fragmentation du CPU et de la mémoire
AkMemoryArena utilise un mélange de différents algorithmes d'allocation qui cherchent à fournir un équilibre entre les performances globales du CPU et l'utilisation de la mémoire dans une variété de scénarios :
- Un allocateur basé sur les blocs est utilisé pour les petites allocations dont la taille est inférieure à 256 octets.
- c'est-à-dire la section « Small-Block Allocator » (ou SBA) de la région (ou arena)
- Un allocateur basé sur une liste libre, qui utilise une politique d'allocation « Good-fit » (« la mieux adaptée »), pour les allocations de taille moyenne
- c'est-à-dire la section « Two-level segregated fit » (ou TLSF) de la région
- Des zones autonomes pour les attributions de grande taille.
Cela devrait permettre aux développeurs de contrôler la fragmentation globale de la mémoire au fil du temps. En outre, les algorithmes d'allocation présentent tous des caractéristiques de performance constantes, ce qui devrait contribuer à maintenir la charge de travail du CPU à un faible niveau dans la plupart des situations.
L'AkMemoryArena n'a pas non plus de cache local pour les threads, et s'appuie plutôt sur des mécanismes de verrouillage légers pour gérer la synchronisation entre les threads. En partageant l'état de l'allocateur mémoire entre tous les threads disponibles, cela devrait permettre de garder la réservation de la mémoire prévisible, même dans les scénarios multithread.
L'absence de mise en cache au niveau des threads présente cependant l'inconvénient que de fréquentes allocations de mémoire entre plusieurs threads peuvent nuire aux performances. Pour atténuer la fréquence des allocations de mémoire, en particulier avec un rendu audio multi-cœur, Wwise utilise maintenant aussi certains allocateurs mémoire spécialisés pour gérer les allocations de mémoire de courte durée, tels que TempAlloc et BookmarkAlloc, qui utilisent leur propre état de thread pour quasiment éliminer le besoin de toute synchronisation cross-thread.
En dehors de la surcharge CPU des allocations de mémoire elles-mêmes, nous avons également constaté qu'il est plus viable d'utiliser des pages géantes (« Huge Pages ») d'une taille d'environ 2 Mio pour le mappage de la mémoire. L'utilisation de pages géantes permet de réduire la fréquence des erreurs du Translation Lookaside Buffer (TLB) sur le CPU. Cela améliore généralement les performances globales du CPU d'environ 10 % par rapport à l'utilisation de pages plus petites d'une taille de 4 Kio ou 16 Kio.
Intégration simple dans les moteurs de jeu
Afin de simplifier l'intégration dans les moteurs de jeu et d'essayer d'éliminer ce point d'erreur ou d'incertitude lors de l'allocation de la mémoire, nous nous sommes assurés que les callbacks utilisés par l'AkMemoryArena pour acquérir et libérer des zones de mémoire étaient aussi simples que possible.
Chaque AkMemoryArena nécessite une paire de callbacks fournis par l'utilisateur pour gérer la mémoire. Un pour allouer des zones de mémoire, et un pour libérer des zones de mémoire.
L'intégration de ces callbacks a très peu d'exigences :
- Il n'y a pas d'exigences particulières quant à la manière dont la mémoire est mappée et démappée
- Il n'y a pas d'exigences concernant l'alignement de la mémoire
- Les seules données fournies lors de la libération des zones de mémoire sont exactement les mêmes que celles renvoyées par les appels précédents pour l'allocation – à la fois l'adresse fournie par l'utilisateur pour l'allocation, ainsi qu'un pointeur sur une userData arbitraire.
Pour illustrer la simplicité des callbacks, une implémentation valide de ces fonctions peut simplement transmettre certains des paramètres de la fonction à std::malloc() et std::free() sans aucune logique supplémentaire. En fait, sur les plateformes Windows et POSIX, il s'agit exactement des versions par défaut de ces hooks dans le moteur audio de Wwise !
Hautement configurable pour s'adapter à différents besoins
La plupart des projets et équipes ont des philosophies différentes sur la façon d'aborder l'utilisation de la mémoire, ou encore certaines équipes peuvent considérer certaines décisions comme nécessaires pour l'utilisation de la mémoire en raison de la nature du contenu développé pour leur jeu. Par exemple,
- Une équipe peut préférer que toute la mémoire de l'application soit allouée et entièrement budgétisée pour chaque système au démarrage, afin de s'assurer que l'utilisation de la mémoire soit extrêmement prévisible.
- Une autre équipe peut préférer que la mémoire soit mappée et non mappée à un niveau très granulaire, de sorte que la mémoire ne puisse être utilisée par d'autres systèmes qu'en cas de besoin.
Le cas échéant, nous avons essayé de nous assurer que nous disposions de suffisamment d'options de configuration pour satisfaire une variété de besoins et de cas d'utilisation différents.
Par exemple, bien que les AkMemoryArenas n'exigent pas que toute la mémoire soit pré-réservée au moment de l'initialisation, il est possible de les configurer de manière à ce qu'ils se comportent effectivement comme tels, en fixant la zone initiale à une taille très importante. Cette zone initiale de mémoire pourrait même être imposée comme une limite souple dans vos callbacks d'allocation mémoire, en émettant un avertissement chaque fois qu'ils sont invoqués après le premier appel.
Surveiller la fragmentation de la mémoire avec le Profiler
Dans le cadre de l'intégration des AkMemoryArenas dans Wwise, nous voulions profiter de l'occasion pour mettre en place des systèmes permettant d'offrir un suivi détaillé et un profilage des régions au fil du temps. Nous nous attendons à ce que cela soit utile pour vous guider dans la configuration des AkMemoryArenas pour vos titres, ainsi que pour nous permettre de fournir un support plus détaillé et de l'aide pour identifier les problèmes ou les opportunités d'optimisation lorsque cela sera nécessaire.
Réduction de l'utilisation de la mémoire
En plus de ces fonctionnalités, nous avons constaté que la mémoire totale réservée, et même la mémoire totale utilisée par l'AkMemoryArena, tend à être beaucoup plus faible que ce qui était réalisable auparavant.
Profilage de l'AkMemoryArena
Le nouveau Profiler pour l'AkMemoryArena fournit les statistiques et données suivantes pour chaque zone de chaque région :
- L'adresse et les données utilisateur renvoyées lors de chaque appel au callback d'allocation, fnMemAllocSpan.
- Notez que chaque ligne de la vue principale représente un appel à fnMemAllocSpan.
- La taille de la zone
- La part de la zone qui a été allouée et celle qui est libre
- Une carte de fragmentation, indiquant quelles parties de la zone sont allouées.
Certaines statistiques sont également fournies pour chaque AkMemoryArena dans son ensemble. En plus de simples accumulations telles que la mémoire totale utilisée et réservée, le Profiler liste également le plus grand espace libre disponible (« Largest Free ») dans la région. Il s'agit d'une statistique indiquant la plus grande allocation que la région peut supporter avant de devoir demander une nouvelle zone. Lorsque cette valeur est comparée à la mémoire libre totale, elle peut être utilisée comme un indicateur général de la fragmentation de la région.
Tout cela est réalisé avec un coût d'exécution minime, et ces données ne nécessitent pas l'historique complet de l'application pour être calculées. Cela signifie que même si un jeu a été exécuté pendant plusieurs heures, l'application de création Wwise sera en mesure de se connecter au jeu et d'évaluer l'état de la disposition de la mémoire avec très peu d'effort.
Il convient de noter que la réduction de la charge de travail du CPU pour le suivi de ces données a pour contrepartie de ne fournir qu'un niveau de détail grossier de l'état des AkMemoryArenas. Bien qu'il ne fournisse pas un niveau de granularité par allocation – comme le font de nombreux outils spécialisés dans l'analyse de la mémoire – nous pensons que ces données devraient être suffisantes pour permettre à un utilisateur d'évaluer facilement s'il y a un problème avec l'utilisation de la mémoire, de l'utiliser pour diriger une inspection plus approfondie si un problème est trouvé, et de guider ses décisions pour une configuration plus poussée des régions et des stratégies de mémoire.
Intégration dans un moteur de jeu
Pour les utilisateurs utilisant un moteur de jeu propriétaire, par opposition à l'utilisation de nos intégrations pré-faites de Wwise dans Unity et Unreal, il serait intéressant de reconsidérer certains aspects de la façon dont le système de mémoire peut être intégré et configuré.
Pour commencer, il faut noter que tous les callbacks existants pour gérer les allocations mémoire individuelles sont toujours disponibles, et le comportement de ceux-ci n'a pas été modifié : Si vous préférez utiliser votre propre allocateur mémoire pour chaque allocation provenant de Wwise, cette option est disponible.
Cependant, l'utilisation de ces callbacks empêchera toute utilisation des AkMemoryArenas ainsi que du nouveau Profiler décrit ci-dessus. Même si vous ressentez le besoin de vous assurer que chaque allocation mémoire est prise en compte, il peut être intéressant d'envisager d'utiliser simplement l'AkMemoryArena à la place, en raison des outils disponibles. S'appuyer sur les AkMemoryArenas pour gérer la plupart des allocations mémoire peut également soulager la pression sur d'autres systèmes de mémoire globale que vous pouvez avoir, ou aider à simplifier le développement d'autres outils pour suivre l'utilisation de la mémoire dans Wwise.
Il est également intéressant de noter que même si l'AkMemoryArena est en cours d'utilisation, il est toujours possible d'enregistrer certaines métadonnées sur chaque allocation mémoire individuelle. Les hooks de mémoire « Debug » dans les AkMemSettings sont disponibles dans ce but :
Ces hooks de mémoire sont actifs indépendamment du fait que Wwise utilise son système d'allocation mémoire intégré, ou que les callbacks d'allocation mémoire individuels soient utilisés à la place. Il peut être utile d'utiliser ces callbacks si un diagnostic approfondi de la fragmentation de la mémoire doit être effectué.
Comme nous l'avons vu précédemment, nous avons essayé de faire en sorte que la configuration initiale et l'intégration des AkMemoryArenas soient aussi simples que possible. Voici un exemple d'implémentation de callbacks pour l'allocation et la libération de la mémoire.
Les AkMemoryArenas étant configurés séparément, il est même possible d'utiliser des callbacks différents pour chaque AkMemoryArena. Cela peut s'avérer utile si vous souhaitez utiliser différents allocateurs mémoire de bas niveau pour les régions Primary ou Media, en raison des différences relatives de durée de vie et de taille des allocations utilisées par ces systèmes.
Par ailleurs, si vous souhaitez désactiver l'utilisation de certaines régions de mémoire, vous pouvez simplement définir les callbacks à nullptr :
Ceci est utilisé pour désactiver la région Profiler (AkMemoryMgrArena_Profiler) dans les configurations par défaut des versions Release officielles, ou pour désactiver la région de mémoire Device (AkMemoryMgrArena_Device) sur les plateformes qui n'utilisent pas la mémoire spécifique au périphérique pour le traitement audio.
De même, selon la façon dont le reste de l'intégration de Wwise est mis en place dans votre moteur de jeu, il se peut que Wwise n'alloue jamais de mémoire à la région Media lors d'une séquence de jeu normale. Cela peut être le cas si votre projet utilise exclusivement des API comme AK::SoundEngine::SetMedia et AK::SoundEngine::LoadBankMemoryView, au lieu de AK::SoundEngine::LoadBank ou AK::SoundEngine::LoadBankMemoryCopy. Notez que certaines allocations Media sont encore effectuées lorsque l'outil de création transfère de nouveaux médias au moteur audio pendant le profilage ; cette option ne devrait donc être considérée que si vous ciblez la configuration Release de Wwise.
Selon l'intégration de votre moteur de jeu, il peut être intéressant d'utiliser également les régions de mémoire à l'extérieur du moteur audio. Par exemple, il est possible de créer sa propre allocation mémoire à l'aide de AK::MemoryMgr::Malloc, de charger les données de votre SoundBank dans cette allocation, puis de fournir cette mémoire à AK::SoundEngine::LoadBankMemoryView. Cela signifierait que la mémoire serait toujours gérée par l'AkMemoryArena, ce qui vous permettrait de bénéficier des avantages du Profiler et d'autres systèmes, tout en permettant à l'allocation mémoire d'appartenir à votre code et d'avoir une durée de vie que vous contrôlez.
Configuration et réglage supplémentaires
Le tableau memoryArenaSettings dans AkMemSettings est utilisé pour configurer d'autres paramètres dans la région de mémoire. Bien que ce tableau soit configuré avec des valeurs par défaut raisonnables dans le moteur audio, afin de s'assurer que les systèmes se comportent bien dans les circonstances les plus courantes, il est bon de se rappeler que chaque jeu a un contenu et des besoins en mémoire différents, et qu'une optimisation appropriée du contenu de votre jeu peut aider à réaliser des améliorations significatives de l'utilisation de la mémoire. Par exemple, lors de nos tests sur des contenus de jeux simulés, nous avons constaté que nous pouvions réduire la mémoire globale réservée de 5 à 10 % en apportant quelques modifications simples aux paramètres d'AkMemoryArena pour chaque test.
Voici quelques suggestions simples que nous recommandons de prendre en compte :
- Définir AkMemoryArenaSettings::uTlsfInitSize pour correspondre à l'utilisation typique de la mémoire ou aux budgets de mémoire ciblés. En règle générale, nous avons constaté qu'une plus grande « Base », ou zone initiale, tend à offrir les meilleures performances en matière de fragmentation de la mémoire au lieu d'avoir de nombreuses zones de mémoire individuelles. Une taille initiale plus importante peut également aider à clarifier la réservation de la mémoire totale du système au démarrage et faciliter la budgétisation de la mémoire dans d'autres domaines.
- Définissez AkMemoryArenaSettings::uSbaInitSize à un watermark identifié dans le Profiler. Le SBA a sa propre zone de « Base » qui présente l'avantage supplémentaire d'avoir une empreinte mémoire réduite pour chaque allocation constitutive : environ 16 octets par allocation. En augmentant la valeur de ce paramètre de manière à ce que davantage de « petites » allocations soient placées dans l'intervalle de base du SBA, on peut réduire non seulement la mémoire réservée, mais aussi la mémoire utilisée.
- Réglez AkMemoryArenaSettings::uAllocSizeHuge à une valeur inférieure pour réduire la fragmentation dans les zones TLSF. Une valeur plus faible ici garantira que plus d'allocations deviennent des zones « Huge » autonomes, au lieu d'être dans les zones TLSF. Notez que cela dépend fortement de la façon dont l'intégration des hooks de zone mémoire pour fnMemAllocSpan est mise en place, car cela entraînera également plus d'appels à fnMemAllocSpan et doit supposer que la fragmentation externe des zones n'est pas un problème.
D'autres suggestions comme celles-ci sont disponibles dans la documentation SDK de Wwise dans Configuration and Tuning of AkMemoryArenas.
Commentaires