Práticas recomendadas para reduzir a atividade do coletor de lixo em Javascript

94

Eu tenho um aplicativo Javascript bastante complexo, que tem um loop principal que é chamado 60 vezes por segundo. Parece haver muita coleta de lixo acontecendo (com base na saída 'dente de serra' da linha do tempo do Memory nas ferramentas de desenvolvimento do Chrome) - e isso geralmente afeta o desempenho do aplicativo.

Portanto, estou tentando pesquisar as melhores práticas para reduzir a quantidade de trabalho que o coletor de lixo tem que fazer. (A maioria das informações que consegui encontrar na web diz respeito a evitar vazamentos de memória, o que é uma questão um pouco diferente - minha memória está sendo liberada, só que há muita coleta de lixo em andamento.) Estou assumindo que isso se resume principalmente em reutilizar objetos tanto quanto possível, mas é claro que o diabo está nos detalhes.

O aplicativo é estruturado em 'classes' ao longo das linhas de Herança JavaScript simples de John Resig .

Acho que um problema é que algumas funções podem ser chamadas milhares de vezes por segundo (visto que são usadas centenas de vezes durante cada iteração do loop principal) e talvez as variáveis ​​de trabalho locais nessas funções (strings, arrays, etc.) pode ser o problema.

Estou ciente do agrupamento de objetos para objetos maiores / mais pesados ​​(e usamos isso até certo ponto), mas estou procurando técnicas que podem ser aplicadas em todos os setores, especialmente relacionadas a funções que são chamadas muitas vezes em loops estreitos .

Que técnicas posso usar para reduzir a quantidade de trabalho que o coletor de lixo deve fazer?

E, talvez também - que técnicas podem ser empregadas para identificar quais objetos estão sendo mais coletados? (É uma base de código muito grande, portanto, comparar os instantâneos do heap não foi muito proveitoso)

Até o riacho
fonte
2
Você tem um exemplo de seu código que poderia nos mostrar? A pergunta será mais fácil de responder então (mas também potencialmente menos geral, então não tenho certeza aqui)
John Dvorak
2
Que tal parar de executar funções milhares de vezes por segundo? Essa é realmente a única maneira de abordar isso? Esta pergunta parece ser um problema XY. Você está descrevendo X, mas o que realmente está procurando é uma solução para Y.
Travis J
2
@TravisJ: Ele roda apenas 60 vezes por segundo, que é uma taxa de animação bastante comum. Ele não pede menos trabalho, mas sim como fazê-lo de maneira mais eficiente na coleta de lixo.
Bergi
1
@Bergi - "algumas funções podem ser chamadas milhares de vezes por segundo". Isso é uma vez por milissegundo (possivelmente pior!). Isso não é comum de todo. 60 vezes por segundo não deve ser um problema. Essa pergunta é muito vaga e só vai produzir opiniões ou suposições.
Travis J
4
@TravisJ - Não é incomum em frameworks de jogos.
UpTheCreek de

Respostas:

127

Muitas coisas que você precisa fazer para minimizar a rotatividade de GC vão contra o que é considerado JS idiomático na maioria dos outros cenários, portanto, tenha em mente o contexto ao julgar o conselho que dou.

A alocação acontece em intérpretes modernos em vários lugares:

  1. Quando você cria um objeto por meio newou por meio da sintaxe literal [...], ou {}.
  2. Quando você concatena strings.
  3. Quando você insere um escopo que contém declarações de função.
  4. Quando você executa uma ação que dispara uma exceção.
  5. Quando você avalia uma expressão de função: (function (...) { ... }).
  6. Quando você executa uma operação que força a Objeto como Object(myNumber)ouNumber.prototype.toString.call(42)
  7. Quando você chama um interno que faz qualquer uma dessas coisas por baixo do capô, como Array.prototype.slice.
  8. Quando você usa argumentspara refletir sobre a lista de parâmetros.
  9. Quando você divide uma string ou combina com uma expressão regular.

Evite fazer isso e agrupe e reutilize objetos sempre que possível.

Especificamente, procure oportunidades para:

  1. Puxe as funções internas que não têm ou têm poucas dependências no estado fechado para um escopo superior e de vida mais longa. (Alguns minificadores de código, como o compilador Closure, podem incorporar funções internas e melhorar o desempenho do GC.)
  2. Evite usar strings para representar dados estruturados ou para endereçamento dinâmico. Evite especialmente analisar repetidamente usando splitou correspondências de expressão regular, pois cada uma requer várias alocações de objetos. Isso freqüentemente acontece com chaves em tabelas de pesquisa e IDs de nós dinâmicos do DOM. Por exemplo, lookupTable['foo-' + x]e document.getElementById('foo-' + x)ambos envolvem uma alocação, pois há uma concatenação de string. Freqüentemente, você pode anexar chaves a objetos de longa duração em vez de reconcatenar. Dependendo dos navegadores que você precisa oferecer suporte, você pode usar Mappara usar objetos como chaves diretamente.
  3. Evite capturar exceções em caminhos de código normais. Em vez de try { op(x) } catch (e) { ... }, faça if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Quando você não puder evitar a criação de strings, por exemplo, para passar uma mensagem para um servidor, use um builtin like JSON.stringifyque usa um buffer nativo interno para acumular conteúdo em vez de alocar vários objetos.
  5. Evite usar callbacks para eventos de alta frequência e, onde for possível, passar como callback uma função de longa duração (consulte 1) que recria o estado do conteúdo da mensagem.
  6. Evite usar as argumentsfunções desde que usam isso para criar um objeto do tipo array quando chamado.

Sugeri usar JSON.stringifypara criar mensagens de rede de saída. Analisar mensagens de entrada usando JSON.parseobviamente envolve alocação, e muito disso para mensagens grandes. Se você pode representar suas mensagens recebidas como matrizes de primitivas, então você pode economizar muitas alocações. O único outro componente integrado ao qual você pode construir um analisador que não aloca é String.prototype.charCodeAt. No entanto, um analisador para um formato complexo que só usa isso vai ser um inferno de ler.

Mike Samuel
fonte
Você não acha que os JSON.parseobjetos d alocam menos (ou igual) espaço do que a string da mensagem?
Bergi
@Bergi, Isso depende se os nomes das propriedades exigem alocações separadas, mas um analisador que gera eventos em vez de uma árvore de análise não faz alocações estranhas.
Mike Samuel
Resposta fantástica, obrigado! Muitas desculpas pelo vencimento da recompensa - Eu estava viajando na época e por alguma razão não consegui entrar no SO com minha conta do gmail no meu telefone ....: /
UpTheCreek
Para compensar o meu tempo ruim com a recompensa, adicionei um adicional para completar (200 era o mínimo que eu poderia dar;) - Por alguma razão, porém, está exigindo que eu espere 24 horas antes de concedê-lo (embora Selecionei 'recompensar resposta existente'). Será seu amanhã ...
UpTheCreek
@UpTheCreek, não se preocupe. Estou feliz que você achou útil.
Mike Samuel
13

As ferramentas de desenvolvedor do Chrome têm um recurso muito bom para rastrear a alocação de memória. É chamado de Linha do Tempo da Memória. Este artigo descreve alguns detalhes. Suponho que é disso que você está falando sobre o "dente de serra"? Este é o comportamento normal para a maioria dos tempos de execução com GC. A alocação continua até que um limite de uso seja atingido, acionando uma coleção. Normalmente, existem diferentes tipos de coleções em diferentes limites.

Linha do tempo da memória no Chrome

As coletas de lixo são incluídas na lista de eventos associada ao rastreamento junto com sua duração. No meu notebook bastante antigo, coletas efêmeras estão ocorrendo em cerca de 4 Mb e levam 30 ms. Isso é 2 de suas iterações de loop de 60Hz. Se esta for uma animação, as coleções de 30ms provavelmente estão causando falhas. Você deve começar aqui para ver o que está acontecendo em seu ambiente: onde está o limite de coleta e quanto tempo suas coletas estão levando. Isso fornece um ponto de referência para avaliar as otimizações. Mas você provavelmente não fará melhor do que diminuir a frequência da gagueira diminuindo a taxa de alocação, aumentando o intervalo entre as coleções.

A próxima etapa é usar a opção Perfis | Recurso Record Heap Allocations para gerar um catálogo de alocações por tipo de registro. Isso mostrará rapidamente quais tipos de objeto estão consumindo mais memória durante o período de rastreamento, o que é equivalente à taxa de alocação. Concentre-se neles em ordem decrescente de taxa.

As técnicas não são ciência de foguetes. Evite objetos encaixotados quando você pode fazer com um não encaixotado. Use variáveis ​​globais para manter e reutilizar objetos em uma única caixa em vez de alocar novos em cada iteração. Agrupe tipos de objetos comuns em listas gratuitas em vez de abandoná-los. Resultados da concatenação de string de cache que provavelmente podem ser reutilizados em iterações futuras. Evite a alocação apenas para retornar os resultados da função definindo variáveis ​​em um escopo delimitador. Você terá que considerar cada tipo de objeto em seu próprio contexto para encontrar a melhor estratégia. Se precisar de ajuda com detalhes, poste uma edição descrevendo os detalhes do desafio que você está olhando.

Aconselho a não perverter seu estilo de codificação normal em um aplicativo em uma tentativa de espingarda de produzir menos lixo. É pela mesma razão que você não deve otimizar a velocidade prematuramente. A maior parte do seu esforço mais a maior complexidade e obscuridade do código não fará sentido.

Gene
fonte
Certo, é isso que quero dizer com dente de serra. Sei que sempre haverá um padrão dente de serra de algum tipo, mas minha preocupação é que, com meu aplicativo, a frequência de dente de serra e os 'penhascos' sejam bastante altos. Curiosamente, eventos GC não aparecem no meu cronograma - os únicos eventos que aparecem no painel 'registros' (o do meio) são: request animation frame, animation frame fired, e composite layers. Não tenho ideia de por que não estou vendo GC Eventcomo você (esta é a última versão do Chrome, e também canário).
UpTheCreek
4
Tentei usar o criador de perfil com 'alocações de heap de registro', mas até agora não achei muito útil. Talvez seja porque eu não sei como usá-lo corretamente. Parece estar cheio de referências que nada significam para mim, como @342342e code relocation info.
UpTheCreek
9

Como princípio geral, você deseja armazenar em cache o máximo possível e criar e destruir o mínimo possível para cada execução do loop.

A primeira coisa que me vem à cabeça é reduzir o uso de funções anônimas (se houver) dentro do seu loop principal. Além disso, seria fácil cair na armadilha de criar e destruir objetos que são passados ​​para outras funções. Não sou um especialista em javascript, mas imagino que este:

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

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

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

seria executado muito mais rápido do que isso:

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

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

Existe algum tempo de inatividade para o seu programa? Talvez você precise que ele funcione sem problemas por um ou dois segundos (por exemplo, para uma animação) e então tenha mais tempo para processar? Se for esse o caso, eu poderia ver pegando objetos que normalmente seriam coletados como lixo em toda a animação e mantendo uma referência a eles em algum objeto global. Então, quando a animação terminar, você pode limpar todas as referências e deixar o coletor de lixo fazer o trabalho.

Desculpe se isso tudo é um pouco trivial em comparação com o que você já tentou e pensou.

Chris B
fonte
Este. Além disso, as funções mencionadas dentro de outras funções (que não são IIFEs) também são um abuso comum que queima muita memória e é fácil de perder.
Esailija
Obrigado Chris! Não tenho nenhum tempo de inatividade, infelizmente: /
UpTheCreek
4

Eu faria um ou alguns objetos no global scope(onde tenho certeza que o coletor de lixo não tem permissão para tocá-los), então tentaria refatorar minha solução para usar esses objetos para fazer o trabalho, em vez de usar variáveis ​​locais .

É claro que isso não poderia ser feito em todo o código, mas geralmente essa é minha maneira de evitar o coletor de lixo.

PS: pode tornar essa parte específica do código um pouco menos sustentável.

Mahdi
fonte
O GC remove minhas variáveis ​​de escopo global de forma consistente.
VectorVortec