Mejores prácticas para reducir la actividad del recolector de basura en Javascript

94

Tengo una aplicación Javascript bastante compleja, que tiene un bucle principal que se llama 60 veces por segundo. Parece que hay una gran cantidad de recolección de basura (según la salida de 'diente de sierra' de la línea de tiempo de la memoria en las herramientas de desarrollo de Chrome), y esto a menudo afecta el rendimiento de la aplicación.

Entonces, estoy tratando de investigar las mejores prácticas para reducir la cantidad de trabajo que tiene que hacer el recolector de basura. (La mayor parte de la información que he podido encontrar en la web se refiere a evitar fugas de memoria, lo cual es una cuestión ligeramente diferente: mi memoria se está liberando, es solo que hay demasiada recolección de basura). que esto se reduce principalmente a reutilizar objetos tanto como sea posible, pero por supuesto, el diablo está en los detalles.

La aplicación está estructurada en 'clases' siguiendo las líneas de Herencia de JavaScript simple de John Resig .

Creo que un problema es que algunas funciones se pueden llamar miles de veces por segundo (ya que se usan cientos de veces durante cada iteración del bucle principal) y quizás las variables de trabajo locales en estas funciones (cadenas, matrices, etc.) podría ser el problema.

Soy consciente de la agrupación de objetos para objetos más grandes / pesados ​​(y usamos esto hasta cierto punto), pero estoy buscando técnicas que se puedan aplicar en todos los ámbitos, especialmente en relación con funciones que se llaman muchas veces en bucles estrechos .

¿Qué técnicas puedo utilizar para reducir la cantidad de trabajo que debe hacer el recolector de basura?

Y, quizás también, ¿qué técnicas se pueden emplear para identificar qué objetos se recolectan más basura? (Es una base de código muy grande, por lo que comparar instantáneas del montón no ha sido muy fructífero)

Hasta el arroyo
fuente
2
¿Tiene un ejemplo de su código que podría mostrarnos? La pregunta será más fácil de responder entonces (pero también potencialmente menos general, así que no estoy seguro aquí)
John Dvorak
2
¿Qué tal dejar de ejecutar funciones miles de veces por segundo? ¿Es esa realmente la única forma de abordar esto? Esta pregunta parece un problema XY. Estás describiendo X, pero lo que realmente estás buscando es una solución para Y.
Travis J
2
@TravisJ: Lo ejecuta solo 60 veces por segundo, que es una frecuencia de animación bastante común. No pide hacer menos trabajo, sino cómo hacerlo de manera más eficiente en la recolección de basura.
Bergi
1
@Bergi - "algunas funciones se pueden llamar miles de veces por segundo". Eso es una vez por milisegundo (¡posiblemente peor!). Eso no es nada común. 60 veces por segundo no debería ser un problema. Esta pregunta es demasiado vaga y solo producirá opiniones o conjeturas.
Travis J
4
@TravisJ: no es nada raro en los marcos de juego.
UpTheCreek

Respuestas:

127

Muchas de las cosas que debe hacer para minimizar la rotación de GC van en contra de lo que se considera JS idiomático en la mayoría de los otros escenarios, así que tenga en cuenta el contexto al juzgar los consejos que doy.

La asignación ocurre en intérpretes modernos en varios lugares:

  1. Cuando crea un objeto mediante newo mediante sintaxis literal [...], o {}.
  2. Cuando concatenas cadenas.
  3. Cuando ingresa a un ámbito que contiene declaraciones de funciones.
  4. Cuando realiza una acción que desencadena una excepción.
  5. Cuando se evalúa una expresión de función: (function (...) { ... }).
  6. Cuando realiza una operación que coacciona a Objeto como Object(myNumber)oNumber.prototype.toString.call(42)
  7. Cuando llamas a un incorporado que hace cualquiera de estos bajo el capó, como Array.prototype.slice.
  8. Cuando usa argumentspara reflexionar sobre la lista de parámetros.
  9. Cuando divide una cadena o coincide con una expresión regular.

Evite hacer eso, y agrupe y reutilice objetos cuando sea posible.

Específicamente, busque oportunidades para:

  1. Extraiga las funciones internas que tienen pocas o ninguna dependencia del estado cerrado hacia un ámbito superior y de mayor duración. (Algunos minificadores de código como el compilador Closure pueden incorporar funciones internas y pueden mejorar el rendimiento de su GC).
  2. Evite el uso de cadenas para representar datos estructurados o para direccionamiento dinámico. Especialmente evite analizar repetidamente el uso de splitcoincidencias de expresiones regulares ya que cada uno requiere múltiples asignaciones de objetos. Esto sucede con frecuencia con claves en tablas de búsqueda e ID de nodo DOM dinámico. Por ejemplo, lookupTable['foo-' + x]y document.getElementById('foo-' + x)ambos implican una asignación, ya que hay una concatenación de cadenas. A menudo, puede adjuntar claves a objetos de larga duración en lugar de volver a concatenarlos. Dependiendo de los navegadores que necesite admitir, es posible que pueda Maputilizar objetos como claves directamente.
  3. Evite la captura de excepciones en las rutas de código normales. En lugar de try { op(x) } catch (e) { ... }hacerlo if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Cuando no puede evitar la creación de cadenas, por ejemplo, para pasar un mensaje a un servidor, utilice un tipo integrado JSON.stringifyque utiliza un búfer nativo interno para acumular contenido en lugar de asignar varios objetos.
  5. Evite el uso de devoluciones de llamada para eventos de alta frecuencia y, cuando pueda, pase como devolución de llamada una función de larga duración (consulte 1) que recrea el estado del contenido del mensaje.
  6. Evite el uso de argumentsfunciones since que usan que tienen que crear un objeto similar a una matriz cuando se llaman.

Sugerí usarlo JSON.stringifypara crear mensajes de red salientes. El análisis de los mensajes de entrada utilizando JSON.parseobviamente implica asignación, y mucho para mensajes grandes. Si puede representar sus mensajes entrantes como matrices de primitivas, puede guardar muchas asignaciones. El único otro componente integrado alrededor del cual puede construir un analizador que no asigna es String.prototype.charCodeAt. Sin embargo, un analizador para un formato complejo que solo usa ese será un infierno de leer.

Mike Samuel
fuente
¿No crees que los JSON.parseobjetos d asignan menos (o igual) espacio que la cadena del mensaje?
Bergi
@Bergi, eso depende de si los nombres de las propiedades requieren asignaciones separadas, pero un analizador que genera eventos en lugar de un árbol de análisis no hace asignaciones extrañas.
Mike Samuel
Fantástica respuesta, ¡gracias! Muchas disculpas por la expiración de la recompensa: estaba viajando en ese momento y, por alguna razón, no pude iniciar sesión en SO con mi cuenta de gmail en mi teléfono ...: /
UpTheCreek
Para compensar mi mala sincronización con la recompensa, agregué una adicional para completarla (200 era el mínimo que podía dar;) - Por alguna razón, aunque me obliga a esperar 24 horas antes de otorgarla (aunque Seleccioné 'recompensar la respuesta existente'). Será tuyo mañana ...
UpTheCreek
@UpTheCreek, no te preocupes. Me alegra que lo hayas encontrado útil.
Mike Samuel
13

Las herramientas para desarrolladores de Chrome tienen una característica muy buena para rastrear la asignación de memoria. Se llama Línea de tiempo de la memoria. Este artículo describe algunos detalles. ¿Supongo que esto es de lo que estás hablando sobre el "diente de sierra"? Este es un comportamiento normal para la mayoría de los tiempos de ejecución con GC. La asignación continúa hasta que se alcanza un umbral de uso que activa una recopilación. Normalmente hay diferentes tipos de colecciones en diferentes umbrales.

Línea de tiempo de memoria en Chrome

Las recolecciones de basura se incluyen en la lista de eventos asociada con el seguimiento junto con su duración. En mi portátil bastante viejo, las colecciones efímeras se producen a unos 4 Mb y tardan 30 ms. Estas son 2 de sus iteraciones de bucle de 60 Hz. Si se trata de una animación, las colecciones de 30 ms probablemente estén causando tartamudeo. Debe comenzar aquí para ver qué está sucediendo en su entorno: dónde está el umbral de recolección y cuánto tiempo están tomando sus recolecciones. Esto le brinda un punto de referencia para evaluar las optimizaciones. Pero probablemente no hará nada mejor que disminuir la frecuencia de la tartamudez al disminuir la tasa de asignación, alargando el intervalo entre colecciones.

El siguiente paso es utilizar los perfiles | Función Record Heap Allocations para generar un catálogo de asignaciones por tipo de registro. Esto mostrará rápidamente qué tipos de objetos consumen más memoria durante el período de seguimiento, lo que equivale a la tasa de asignación. Concéntrese en estos en orden descendente de tasa.

Las técnicas no son ciencia espacial. Evite los objetos en caja cuando puede hacerlo con uno sin caja. Utilice variables globales para retener y reutilizar objetos de un solo cuadro en lugar de asignar nuevos en cada iteración. Reúna tipos de objetos comunes en listas gratuitas en lugar de abandonarlos. Almacene en caché los resultados de la concatenación de cadenas que probablemente sean reutilizables en iteraciones futuras. Evite la asignación solo para devolver resultados de función estableciendo variables en un ámbito adjunto. Deberá considerar cada tipo de objeto en su propio contexto para encontrar la mejor estrategia. Si necesita ayuda con detalles específicos, publique una edición que describa los detalles del desafío que está viendo.

Aconsejo no pervertir su estilo de codificación normal a lo largo de una aplicación en un intento por producir menos basura. Esto es por la misma razón por la que no debe optimizar la velocidad antes de tiempo. La mayor parte de su esfuerzo más gran parte de la complejidad y oscuridad añadidas del código no tendrán sentido.

Gene
fuente
Bien, eso es lo que quiero decir con diente de sierra. Sé que siempre habrá algún tipo de patrón de dientes de sierra, pero mi preocupación es que con mi aplicación la frecuencia de dientes de sierra y los 'acantilados' son bastante altos. Curiosamente, eventos de recolección no aparecen en mi línea de tiempo - los únicos eventos que aparecen en el panel de 'registros' (la del medio) son: request animation frame, animation frame firedy composite layers. No tengo idea de por qué no veo GC Eventcomo tú (esto es en la última versión de Chrome, y también en canary).
UpTheCreek
4
Intenté usar el generador de perfiles con 'asignaciones de montón de registros' pero hasta ahora no lo he encontrado muy útil. Quizás esto se deba a que no sé cómo usarlo correctamente. Parece estar lleno de referencias que no significan nada para mí, como @342342y code relocation info.
UpTheCreek
9

Como principio general, querrá almacenar en caché tanto como sea posible y hacer la menor cantidad de creación y destrucción para cada ejecución de su ciclo.

Lo primero que me viene a la cabeza es reducir el uso de funciones anónimas (si las tiene) dentro de su bucle principal. También sería fácil caer en la trampa de crear y destruir objetos que pasan a otras funciones. De ninguna manera soy un experto en javascript, pero me imagino que esto:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

correría mucho más rápido que esto:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

¿Hay algún tiempo de inactividad para su programa? ¿Quizás necesite que se ejecute sin problemas durante uno o dos segundos (por ejemplo, para una animación) y luego tenga más tiempo para procesar? Si este es el caso, podría ver tomar objetos que normalmente serían basura recolectada a lo largo de la animación y mantener una referencia a ellos en algún objeto global. Luego, cuando termine la animación, puede borrar todas las referencias y dejar que el recolector de basura haga su trabajo.

Lo siento si todo esto es un poco trivial en comparación con lo que ya ha probado y pensado.

Chris B
fuente
Esta. Además, las funciones mencionadas dentro de otras funciones (que no son IIFE) también son un abuso común que quema mucha memoria y es fácil de perder.
Esailija
Gracias Chris! Desafortunadamente, no tengo tiempo de inactividad: /
UpTheCreek
4

Haría uno o algunos objetos en el global scope(donde estoy seguro de que el recolector de basura no puede tocarlos), luego trataría de refactorizar mi solución para usar esos objetos para hacer el trabajo, en lugar de usar variables locales .

Por supuesto, no se podría hacer en todas partes del código, pero generalmente esa es mi manera de evitar el recolector de basura.

PD: Podría hacer que esa parte específica del código sea un poco menos fácil de mantener.

Mahdi
fuente
El GC saca mis variables de alcance global de manera consistente.
VectorVortec