Comment vous préparez-vous aux conditions de mémoire insuffisante?

18

Cela peut être facile pour les jeux avec une portée bien définie, mais la question concerne les jeux sandbox, où le joueur est autorisé à créer et à construire n'importe quoi .

Techniques possibles:

  • Utilisez des pools de mémoire avec limite supérieure.
  • Supprimez périodiquement les objets dont vous n'avez plus besoin.
  • Allouez une quantité supplémentaire de mémoire au début afin qu'elle puisse être libérée plus tard en tant que mécanisme de récupération. Je dirais environ 2 à 4 Mo.

Cela est plus susceptible de se produire dans les plates-formes mobiles / consoles où la mémoire est généralement limitée contrairement à votre PC de 16 Go. Je suppose que vous avez un contrôle total sur l'allocation / désallocation de mémoire et aucune collecte de déchets impliquée. C'est pourquoi je marque cela en C ++.

Veuillez noter que je ne parle pas de l'élément efficace C ++ 7 "Soyez prêt pour les conditions de mémoire insuffisante" , même s'il est pertinent, j'aimerais voir une réponse plus liée au développement de jeux, où vous avez généralement plus de contrôle sur ce qui est événement.

Pour résumer la question, comment vous préparez-vous aux conditions de mémoire insuffisante pour les jeux sandbox, lorsque vous ciblez une plate-forme avec une console mémoire / mobile limitée?

concept3d
la source
Les allocations de mémoire en échec sont assez rares sur les systèmes d'exploitation PC modernes, car elles seront automatiquement échangées sur le disque dur lors de la panne de RAM physique. Encore une situation à éviter, car le swapping est beaucoup plus lent que la RAM physique et aura un impact important sur les performances.
Philipp
@Philipp oui je sais. Mais ma question concerne plus les appareils à mémoire limitée tels que les consoles et les mobiles, je pense que je l'ai mentionné.
concept3d
C'est une question assez large (et une sorte de sondage tel qu'il est formulé). Pouvez-vous réduire un peu la portée pour être plus spécifique à une seule situation?
MichaelHouse
@ Byte56 J'ai édité la question. J'espère qu'il a maintenant une portée plus définie.
concept3d

Réponses:

16

En règle générale, vous ne gérez pas la mémoire insuffisante. La seule option sensée dans un logiciel aussi grand et complexe qu'un jeu est de simplement planter / affirmer / terminer dans votre allocateur de mémoire dès que possible (en particulier dans les versions de débogage). Les conditions de mémoire insuffisante sont testées et gérées dans certains logiciels système de base ou logiciels de serveur dans certains cas, mais généralement pas ailleurs.

Lorsque vous avez un plafond de mémoire supérieur, assurez-vous simplement de ne jamais avoir besoin de plus que cette quantité de mémoire. Vous pouvez conserver un nombre maximum de PNJ autorisés à la fois, par exemple, et simplement arrêter de faire apparaître de nouveaux PNJ non essentiels une fois ce plafond atteint. Pour les PNJ essentiels, vous pouvez soit les faire remplacer les non-essentiels, soit avoir un pool / cap séparé pour les PNJ essentiels que vos concepteurs savent concevoir (par exemple, si vous ne pouvez avoir que 3 PNJ essentiels, les concepteurs n'en mettront pas plus de 3 dans une zone / un morceau - de bons outils aideront les concepteurs à le faire correctement et les tests sont bien sûr essentiels).

Un très bon système de streaming est également important, en particulier pour les jeux sandbox. Vous n'avez pas besoin de garder tous les PNJ et objets en mémoire. Au fur et à mesure que vous vous déplacez à travers des morceaux du monde, de nouveaux morceaux seront diffusés et de vieux morceaux diffusés. Ceux-ci incluront généralement des PNJ et des objets ainsi que du terrain. Les limites de conception et d'ingénierie sur les limites des objets doivent être définies en gardant ce système à l'esprit, sachant qu'au plus X anciens morceaux seront conservés et chargés de manière proactive Y de nouveaux morceaux seront chargés, de sorte que le jeu doit avoir de l'espace pour tout garder les données de X + Y + 1 morceaux en mémoire.

Certains jeux tentent de gérer les situations de mémoire insuffisante avec une approche en deux passes. En gardant à l'esprit que la plupart des jeux ont beaucoup de données mises en cache techniquement inutiles (par exemple, les anciens morceaux mentionnés ci-dessus) et une allocation de mémoire pourrait faire quelque chose comme:

allocate(bytes):
  if can_allocate(bytes):
    return internal_allocate(bytes)
  else:
    warning(LOW_MEMORY)
    tell_systems_to_dump_caches()

    if can_allocate(bytes):
      return internal_allocate(bytes)
    else:
      fatal_error(OUT_OF_MEMORY)

Il s'agit d'une mesure de dernier recours pour faire face à des situations inattendues dans la version, mais pendant le débogage et les tests, vous devriez probablement immédiatement planter. Vous ne voulez pas avoir à vous fier à ce genre de choses (surtout parce que le vidage des caches peut avoir de graves conséquences sur les performances).

Vous pouvez également envisager de vider des copies haute résolution de certaines données, par exemple, vous pouvez vider les niveaux de textures mipmap à plus haute résolution si vous manquez de mémoire GPU (ou de toute mémoire dans une architecture à mémoire partagée). Cela nécessite généralement beaucoup de travail architectural pour en valoir la peine.

Notez que certains jeux sandbox très illimités peuvent être assez facilement simplement plantés, même sur PC (rappelez-vous que les applications 32 bits courantes ont une limite de 2-3 Go d'espace d'adressage même si vous avez un PC avec 128 Go de RAM; un 64- le système d'exploitation et le matériel bit permettent à plus d'applications 32 bits de s'exécuter simultanément mais ne peuvent rien faire pour qu'un binaire 32 bits ait un espace d'adressage plus grand). En fin de compte, soit vous avez un monde de jeu très flexible qui aura besoin d'espace mémoire illimité pour fonctionner dans tous les cas, soit vous avez un monde très limité et contrôlé qui fonctionne toujours parfaitement dans la mémoire limitée (ou quelque chose entre les deux).

Sean Middleditch
la source
+1 pour cette réponse. J'ai écrit deux systèmes qui fonctionnent en utilisant le style de Sean et des pools de mémoire discrets et ils ont tous deux bien fonctionné en production. Le premier était un géniteur qui faisait reculer la sortie sur une courbe jusqu'à la limite maximale d'arrêt afin que le joueur ne remarque jamais une réduction soudaine (pensant que le débit total était abaissé de cette marge de sécurité). La seconde était liée aux morceaux dans la mesure où une allocation échouée forcerait les purges et la réaffectation. Je pense que ** un monde très limité et contrôlé qui fonctionne toujours parfaitement dans une mémoire limitée ** est vital pour tout client de longue date.
Patrick Hughes
+1 pour la mention d'être aussi agressif avec la gestion des erreurs dans les versions de débogage que possible. N'oubliez pas que sur le matériel de la console de débogage, vous avez parfois accès à plus de ressources que la vente au détail. Vous souhaiterez peut-être imiter ces conditions sur le matériel de développement en allouant des objets de débogage exclusivement dans l'espace d'adressage au-dessus des périphériques de vente au détail et en plantant lorsque l'espace d'adressage équivalent au détail est épuisé.
FlintZA
5

L'application est généralement testée sur la plate-forme ciblée avec les pires scénarios et vous serez toujours prêt pour la plate-forme que vous ciblez. Idéalement, l'application ne devrait jamais planter, mais à part l'optimisation pour des appareils spécifiques, il y a peu de choix lorsque vous êtes confronté à un avertissement de mémoire insuffisante.

La meilleure pratique est d'avoir des pools préalloués et le jeu utilise dès le début toute la mémoire nécessaire. Si votre jeu a un maximum de 100 unités, alors ayez un pool de 100 unités et c'est tout. Si 100 unités dépassent les exigences mem pour un appareil ciblé, vous pouvez optimiser l'unité pour utiliser moins de mémoire ou modifier la conception à un maximum de 90 unités. Il ne devrait pas y avoir de cas où vous pouvez construire des choses illimitées, il devrait toujours y avoir une limite. Il serait très mauvais pour un jeu sandbox d'utiliser newpour chaque instance car vous ne pouvez jamais prédire l'utilisation de mem et un crash est bien pire qu'une limitation.

De plus, la conception du jeu doit toujours avoir à l'esprit les appareils les plus bas car si vous basez votre conception avec des choses "illimitées", il sera beaucoup plus difficile de résoudre les problèmes de mémoire ou de modifier la conception plus tard.

Raxvan
la source
1

Eh bien, vous pouvez allouer environ 16 Mio (juste pour être sûr à 100%) au démarrage ou même .bssau moment de la compilation, et utiliser un "allocateur sûr", avec une signature comme inline __attribute__((force_inline)) void* alloc(size_t size)( __attribute__((force_inline))est un GCC / mingw-w64attribut qui force l'inclusion de sections de code critiques même si les optimisations sont désactivées, même si elles devraient être activées pour les jeux) au lieu de malloccela essaie void* result = malloc(size)et si cela échoue, supprimez les caches, libérez la mémoire de rechange (ou dites à un autre code d'utiliser la .bsschose mais cela est hors de portée pour cette réponse) et vider les données non enregistrées (enregistrez le monde sur le disque, si vous utilisez un concept de morceaux de type Minecraft, appelez quelque chose comme saveAllModifiedChunks()). Ensuite, si malloc(16777216)(l'allocation de ces 16 Mio à nouveau) échoue (à nouveau, remplacez par analogique pour .bss), mettez fin au jeu et affichezMessageBox(NULL, "*game name* couldn't continue because of lack of free memory, but your world was safely saved. Try closing background applications and restarting the game", "*Game name*: out of memory", MB_ICONERROR)ou une alternative spécifique à la plate-forme. Mettre tous ensemble:

__attribute__((force_inline)) void* alloc(size_t size) {
    void* result = malloc(size); // Attempt to allocate normally
    if (!result) { // If the allocation failed...
        if (!reserveMemory) std::_Exit(); // If alloc() was called from forceFullSave() or reportOutOfMemory() and we again can't allocate, just quit, something is stealing all our memory. If we used the .bss approach, this wouldn't've been necessary.
        free(reserveMemory); // Global variable, pointer to the reserve 16 MiB allocated on startup
        forceFullSave(); // Saves the game
        reportOutOfMemory(); // Platform specific error message box code
        std::_Exit(); // Close silently
    } else return result;
}

Vous pouvez utiliser une solution similaire avec std::set_new_handler(myHandler)myHandlerest void myHandler(void)appelé en cas d' newéchec:

void newerrhandler() {
    if (!reserveMemory) std::_Exit(); // If new was called from forceFullSave() or reportOutOfMemory() and we again can't allocate, just quit, something is stealing all our memory. If we used the .bss approach, this wouldn't've been necessary.
    free(reserveMemory); // Global variable, pointer to the reserve 16 MiB allocated on startup
    forceFullSave(); // Saves the game
    reportOutOfMemory(); // Platform specific error message box code
    std::_Exit(); // Close silently
}

// In main ()...
std::set_new_handler(newerrhandler);
Vladislav Toncharov
la source