nodejs - Mejores prácticas para reducir la actividad del recolector de basura en Javascript
liberar memoria nodejs (4)
Tengo una aplicación Javascript bastante compleja, que tiene un ciclo principal que se llama 60 veces por segundo. Parece que se está llevando a cabo una gran cantidad de recolección de basura (basada en 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.
Por lo tanto, estoy tratando de investigar las mejores prácticas para reducir la cantidad de trabajo que el recolector de basura tiene que hacer. (La mayor parte de la información que he podido encontrar en la web se refiere a evitar pérdidas de memoria, que es una pregunta un tanto diferente: mi memoria se está liberando, es solo que hay demasiada recolección de basura). Estoy asumiendo que esto se reduce a reutilizar objetos tanto como sea posible, pero por supuesto el diablo está en los detalles.
La aplicación está estructurada en ''clases'' a lo largo de la 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 ciclo 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 / más pesados (y lo usamos hasta cierto punto), pero estoy buscando técnicas que puedan aplicarse en general, especialmente relacionadas con funciones que se llaman muchas veces en bucles apretados .
¿Qué técnicas puedo usar 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 están recogiendo más basura? (Es una base de código muy grande, por lo que la comparación de instantáneas del montón no ha sido muy fructífera)
Como principio general, querría almacenar en la memoria caché tanto como sea posible y hacer tan poca creación y destrucción para cada ejecución de su ciclo.
Lo primero que me viene a la mente es reducir el uso de funciones anónimas (si tiene alguna) dentro de su ciclo principal. También sería fácil caer en la trampa de crear y destruir objetos que pasan a otras funciones. No 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);
}
funcionarí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
});
}
¿Alguna vez hay algún tiempo de inactividad para su programa? Tal vez necesita que funcione sin problemas durante uno o dos segundos (por ejemplo, para una animación) y luego tiene más tiempo para procesar? Si este es el caso, podría ver tomar objetos que normalmente serían basura recogida a lo largo de la animación y mantener una referencia a ellos en algún objeto global. Luego, cuando finaliza la animación, puede borrar todas las referencias y dejar que el recolector de basura lo haga.
Lo siento si esto es un poco trivial en comparación con lo que ya has intentado y pensado.
Crearía uno o pocos 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 que no se puede hacer en todas partes en el código, pero en general esa es mi manera de evitar el recolector de basura.
PS: podría hacer que esa parte específica del código sea menos fácil de mantener.
Las herramientas de desarrollo de Chrome tienen una función muy útil para rastrear la asignación de memoria. Se llama la línea de tiempo de la memoria. Este artículo describe algunos detalles. ¿Supongo que esto es de lo que estás hablando con respecto al "diente de sierra"? Este es un comportamiento normal para la mayoría de los tiempos de ejecución de GC. La asignación continúa hasta que se alcanza un umbral de uso que desencadena una colección. Normalmente hay diferentes tipos de colecciones en diferentes umbrales.
Las colecciones de basura se incluyen en la lista de eventos asociada con el seguimiento junto con su duración. En mi viejo cuaderno, las colecciones efímeras están ocurriendo a aproximadamente 4Mb y toman 30ms. Esta es 2 de tus iteraciones de bucle de 60Hz. Si se trata de una animación, las colecciones de 30 ms probablemente causen tartamudeo. Debe comenzar aquí para ver qué ocurre en su entorno: dónde se encuentra el umbral de recopilación y cuánto tiempo están tomando sus colecciones. Esto le proporciona un punto de referencia para evaluar optimizaciones. Pero probablemente no sea mejor que disminuir la frecuencia del tartamudeo al reducir la velocidad de asignación, alargando el intervalo entre las colecciones.
El siguiente paso es usar los perfiles | Característica Record Heap Allocations para generar un catálogo de asignaciones por tipo de registro. Esto mostrará rápidamente qué tipos de objetos consumen la mayor cantidad de memoria durante el período de rastreo, que es equivalente a la tasa de asignación. Enfóquese en estos en orden descendente de ritmo.
Las técnicas no son ciencia espacial. Evite los objetos en caja cuando puede hacerlo con uno sin caja. Use variables globales para mantener y reutilizar objetos de caja única en lugar de asignar nuevos en cada iteración. Agrupe tipos de objetos comunes en listas libres en lugar de abandonarlos. Resultados de concatenación de cadenas de caché que probablemente sean reutilizables en futuras iteraciones. Evite la asignación solo para devolver resultados de funciones 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, publique una edición que describa los detalles del desafío que está mirando.
Aconsejo no pervertir tu estilo de codificación normal a través de una aplicación en un intento de disparo para producir menos basura. Esto es por la misma razón por la cual no debe optimizar la velocidad prematuramente. La mayor parte de tu esfuerzo sumada a gran parte de la complejidad y oscuridad añadida del código no tendrá sentido.
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 el consejo que doy.
La asignación ocurre en intérpretes modernos en varios lugares:
- Cuando crea un objeto a través de una sintaxis
new
o vía[...]
literal, o{}
. - Cuando concatena cadenas.
- Cuando ingresa un ámbito que contiene declaraciones de funciones.
- Cuando realiza una acción que desencadena una excepción.
- Cuando evalúa una expresión de función:
(function (...) { ... })
. - Cuando realiza una operación que coacciona a Object like
Object(myNumber)
oNumber.prototype.toString.call(42)
- Cuando llamas a un built-in que hace alguno de estos bajo el capó, como
Array.prototype.slice
. - Cuando usa
arguments
para reflejar sobre la lista de parámetros. - Cuando divide una cadena o una coincidencia con una expresión regular.
Evite hacer eso y junte y reutilice los objetos donde sea posible.
Específicamente, busque oportunidades para:
- Tire de las funciones internas que no tienen o tienen pocas dependencias del estado cerrado hacia un alcance superior y de mayor duración. (Algunos minificadores de código como el compilador Closure pueden alinear funciones internas y pueden mejorar el rendimiento de su GC.)
- Evite utilizar cadenas para representar datos estructurados o para el direccionamiento dinámico. Especialmente evite el análisis repetido utilizando coincidencias de expresiones regulares o
split
ya que cada una requiere múltiples asignaciones de objetos. Esto ocurre frecuentemente con las claves en las tablas de búsqueda y las ID dinámicas de los nodos DOM. Por ejemplo,lookupTable[''foo-'' + x]
ydocument.getElementById(''foo-'' + x)
implican una asignación ya que hay una concatenación de cadenas. A menudo puede adjuntar claves a objetos de larga vida en lugar de volver a concatenar. Dependiendo de los navegadores que necesite admitir, es posible que pueda usarMap
para usar objetos como claves directamente. - Evite capturar excepciones en rutas de código normales. En lugar de
try { op(x) } catch (e) { ... }
, hazif (!opCouldFailOn(x)) { op(x); } else { ... }
if (!opCouldFailOn(x)) { op(x); } else { ... }
. - Cuando no puede evitar la creación de cadenas, por ejemplo, para pasar un mensaje a un servidor, use un comando incorporado como
JSON.stringify
que utiliza un búfer nativo interno para acumular contenido en lugar de asignar varios objetos. - Evite utilizar devoluciones de llamada para eventos de alta frecuencia, y cuando pueda, pase como una devolución de llamada una función de larga duración (consulte 1) que recrea el estado del contenido del mensaje.
- Evite usar
arguments
ya que las funciones que lo usan tienen que crear un objeto similar a una matriz cuando se le llama.
Sugerí usar JSON.stringify
para crear mensajes de red salientes. El análisis de los mensajes de entrada utilizando JSON.parse
obviamente implica asignación, y muchos de ellos para mensajes grandes. Si puede representar sus mensajes entrantes como matrices de primitivas, entonces puede guardar muchas asignaciones. La única otra construcción alrededor de la cual puedes construir un analizador que no asigna es String.prototype.charCodeAt
. Sin embargo, un analizador sintáctico para un formato complejo que solo utiliza será infernal de leer.