¿Cómo te preparas para las condiciones de falta de memoria?

18

Esto puede ser fácil para juegos con un alcance bien definido, pero la pregunta es sobre los juegos de sandbox, donde el jugador puede crear y construir cualquier cosa .

Posibles técnicas:

  • Use grupos de memoria con límite superior.
  • Eliminar objetos que ya no son necesarios periódicamente.
  • Asigne una cantidad adicional de memoria al principio para que pueda liberarse más tarde como mecanismo de recuperación. Yo diría que alrededor de 2-4 MB.

Es más probable que esto suceda en plataformas móviles / consolas donde la memoria generalmente es limitada a diferencia de su PC de 16 GB. Supongo que tiene control total sobre la asignación / desasignación de memoria y que no hay recolección de basura involucrada. Es por eso que etiqueto esto como C ++.

Tenga en cuenta que no estoy hablando del artículo 7 de C ++ eficaz "Prepárese para condiciones de falta de memoria" , aunque es relevante, me gustaría ver una respuesta más relacionada con el desarrollo del juego, donde generalmente tiene más control sobre lo que sucediendo.

Para resumir la pregunta, ¿cómo se prepara para las condiciones de falta de memoria para los juegos de sandbox, cuando se dirige a una plataforma con consola / móvil con memoria limitada?

concepto3d
fuente
Las asignaciones de memoria fallidas son bastante raras en los sistemas operativos de PC modernos, ya que se cambiarán automáticamente al disco duro cuando se quede sin RAM física. Todavía es una situación que debe evitarse, ya que el intercambio es mucho más lento que la RAM física y afectará gravemente el rendimiento.
Philipp
@Philipp sí lo sé. Pero mi pregunta es más sobre dispositivos con memoria limitada, como consolas y móviles, creo que mencioné eso.
concept3d
Esta es una pregunta bastante amplia (y una especie de encuesta en la forma en que está redactada). ¿Puedes reducir un poco el alcance para ser más específico a una sola situación?
MichaelHouse
@ Byte56 Edité la pregunta. Espero que tenga un alcance más definido ahora.
concept3d

Respuestas:

16

En general, no maneja la falta de memoria. La única opción sensata en un software tan grande y complejo como un juego es simplemente bloquear / afirmar / terminar en su asignador de memoria lo antes posible (especialmente en las compilaciones de depuración). Las condiciones de falta de memoria se prueban y manejan en algunos software de sistema central o software de servidor en algunos casos, pero no en otros lugares.

Cuando tienes un límite de memoria superior, solo asegúrate de nunca necesitar más que esa cantidad de memoria. Puede mantener un número máximo de NPC permitidos a la vez, por ejemplo, y simplemente dejar de generar nuevos NPC no esenciales una vez que se alcanza ese límite. Para los NPC esenciales, puede hacer que reemplacen los no esenciales o tener un grupo / límite separado para los NPC esenciales que sus diseñadores saben diseñar (por ejemplo, si solo puede tener 3 NPCsa esenciales, los diseñadores no pondrán más de 3 en un área / fragmento: buenas herramientas ayudarán a los diseñadores a hacer esto correctamente y las pruebas son esenciales, por supuesto).

Un sistema de transmisión realmente bueno también es importante, especialmente para los juegos de sandbox. No es necesario mantener todos los NPC y elementos en la memoria. A medida que te mueves por trozos del mundo, se introducirán nuevos fragmentos y fragmentos antiguos. Estos generalmente incluirán NPC y artículos, así como el terreno. El diseño y los límites de ingeniería en los límites de elementos deben establecerse teniendo en cuenta este sistema, sabiendo que a lo sumo X se conservarán trozos viejos y se cargarán proactivamente Y se cargarán trozos nuevos, por lo que el juego debe tener espacio para guardar todos los datos de los fragmentos X + Y + 1 en la memoria.

Algunos juegos intentan manejar situaciones de falta de memoria con un enfoque de dos pasos. Teniendo en cuenta que la mayoría de los juegos tienen muchos datos almacenados en caché técnicamente innecesarios (por ejemplo, los fragmentos anteriores mencionados anteriormente) y una asignación de memoria podría hacer algo como:

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)

Esta es una medida de última parada para hacer frente a situaciones inesperadas en el lanzamiento, pero durante la depuración y las pruebas, probablemente debería bloquearse inmediatamente. No querrás tener que depender de este tipo de cosas (especialmente porque deshacerse de los cachés puede tener algunas consecuencias serias en el rendimiento).

También puede considerar descargar copias de alta resolución de algunos datos, por ejemplo, puede volcar los niveles de texturas mipmap de mayor resolución si se está quedando sin memoria GPU (o cualquier memoria en una arquitectura de memoria compartida). Sin embargo, esto generalmente requiere mucho trabajo arquitectónico para que valga la pena.

Tenga en cuenta que algunos juegos de sandbox muy ilimitados se pueden bloquear fácilmente, incluso en PC (recuerde que las aplicaciones comunes de 32 bits tienen un límite de 2-3 GB de espacio de direcciones, incluso si tiene una PC con 128 GB de RAM; un 64- bit OS y hardware permiten que más aplicaciones de 32 bits se ejecuten simultáneamente, pero no pueden hacer nada para que un binario de 32 bits tenga un espacio de direcciones más grande). Al final, tienes un mundo de juego muy flexible que necesitará un espacio de memoria ilimitado para ejecutarse en todos los casos o tienes un mundo muy limitado y controlado que siempre funciona perfectamente en la memoria limitada (o algo intermedio).

Sean Middleditch
fuente
+1 por esta respuesta. Escribí dos sistemas que funcionan con el estilo de Sean y los grupos de memoria discretos y ambos terminaron funcionando bien en la producción. Primero fue un generador que redujo la salida en una curva hasta el límite máximo de cierre para que el jugador nunca notara una reducción repentina (pensó que el rendimiento total se redujo por ese margen de seguridad). El segundo estaba relacionado con los fragmentos en que una asignación fallida forzaría purgas y reasignaciones. Siento que ** un mundo muy limitado y controlado que siempre funciona perfectamente en memoria limitada ** es vital para cualquier cliente de larga ejecución.
Patrick Hughes
+1 por mencionar que es lo más agresivo posible con el manejo de errores en las compilaciones de depuración. Recuerde que en el hardware de la consola de depuración a veces tiene acceso a más recursos que el minorista. Es posible que desee imitar esas condiciones en el hardware de desarrollo mediante la asignación de objetos de depuración exclusivamente en el espacio de direcciones por encima de lo que tendrían los dispositivos minoristas, y se bloquea cuando se agota el espacio de direcciones equivalente al minorista.
FlintZA
5

La aplicación generalmente se prueba en la plataforma objetivo con los peores escenarios y siempre estará preparado para la plataforma a la que se dirige. Idealmente, la aplicación nunca debería bloquearse, pero aparte de la optimización para dispositivos específicos, existen pocas opciones cuando se enfrenta una advertencia de poca memoria.

La mejor práctica es tener grupos previamente asignados y el juego utiliza desde el principio toda la memoria necesaria. Si su juego tiene un máximo de 100 unidades que tiene un grupo de 100 unidades y eso es todo. Si 100 unidades exceden los requisitos de memoria para un dispositivo específico, entonces puede optimizar la unidad para usar menos memoria o cambiar el diseño a un máximo de 90 unidades. No debe haber ningún caso en el que pueda construir cosas ilimitadas, siempre debe haber un límite. Sería muy malo usar un juego de sandbox newpara cada instancia porque nunca puedes predecir el uso de mem y un bloqueo es mucho peor que una limitación.

Además, el diseño del juego siempre debe tener en cuenta los dispositivos con el objetivo más bajo porque si basa su diseño con elementos "ilimitados", será mucho más difícil resolver los problemas de memoria o cambiar el diseño más adelante.

Raxvan
fuente
1

Bueno, puede asignar alrededor de 16 MiB (solo para estar 100% seguro) al inicio o incluso en el .bssmomento de la compilación, y usar un "asignador seguro", con una firma como inline __attribute__((force_inline)) void* alloc(size_t size)( __attribute__((force_inline))es un GCC / mingw-w64atributo que fuerza la inserción de secciones críticas de código) incluso si las optimizaciones están deshabilitadas, aunque deberían estar habilitadas para los juegos) en lugar de mallocintentarlo void* result = malloc(size)y si falla, suelte los cachés, libere la memoria de reserva (o dígale a otro código que use la .bsscosa pero está fuera del alcance de esta respuesta) vaciar datos no guardados (guarde el mundo en el disco, si usa un concepto de fragmentos similar a Minecraft, llame a algo así saveAllModifiedChunks()). Luego, si malloc(16777216)(al asignar estos 16 MiB nuevamente) falla (nuevamente, reemplácelo por análogo .bss), finalice el juego y muestreMessageBox(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)o una alternativa específica de plataforma. Poniendolo todo junto:

__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;
}

Puede usar una solución similar con std::set_new_handler(myHandler)dónde myHandlerse void myHandler(void)llama cuando newfalla:

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
fuente