Escrevendo Javascript rápido para games e aplicações interativas - Javascript - Artigos - Articles - Fórum
Artigo: Escrevendo Javascript rápido para games e aplicações interativas
Submitted by Cesar_rso , Mar 07 2014 05:32 | Last updated Mar 07 2014 05:32
por Mohsen Heydari
As versões recentes das engines de JavaScript são projetadas para executar grandes blocos de código muito rápido, mas se você não sabe como engines de JavaScript funcionam internamente você pode facilmente degradar o desempenho do aplicativo. É especialmente verdadeiro para os jogos, que precisam de cada gota de desempenho que conseguirem.
Neste artigo vou tentar explicar alguns métodos de otimização comuns de código JavaScript que eu pego caindo no desenvolvimento de meus projetos.
Um dos maiores problemas em ter uma experiência suave reside nas pausas do garbage collector do Javascript. No JavaScript, você cria objetos, mas você não libera-os explicitamente. Esse é o trabalho do coletor de lixo.
O problema surge quando GC decide limpar seus objetos: a execução é pausada, o GC decide que objetos que não são mais necessários e, em seguida, libera-os.
Para manter a seu framerate consistente, você deve manter a criação de lixo (na memória) o mais baixo possível. Muitas vezes, os objetos são criados com a palavra-chave new, por exemplo, new Image(), mas há outras construções que alocam memória implícitamente:
var bar = []; //Creates new array object
Você deve evitar a criação de objetos em loops apertados (loop de renderização por exemplo). Tente alocar objetos uma vez e reutilizá-los mais tarde. Em linguagens como C++, desenvolvedores às vezes usam object pools para evitar golpes de desempenho associados à alocação de memória e fragmentação. A mesma idéia pode ser usada em JavaScript, para evitar pausas do GC. Aqui você pode encontrar mais informações sobre object pools [http://gameprogrammi...bject-pool.html].
Para melhor demonstrar a criação implicita de lixo, considere a seguinte função:
return {bar: "foo"};
Cada vez que esta função é chamada, ela cria um novo objeto anônimo que precisa ser limpo em algum momento. Outro ponto de performance vem do uso do comando [] para limpar sua matriz:
var r = new Array("foo", "bar"); //New array filled with some values, same as ["foo", "bar"]
r = [];//Clear the array
Como você pode ver, a segunda linha cria uma nova matriz e marca a anterior como lixo. É melhor para definir o comprimento de matriz para 0:
As funções podem acordar o GC também. Considere o seguinte:
No código acima, retornamos uma referência de função da nossa função foo mas nós também alocamos memória para a nossa função anônima. O código acima poderia ser reescrito para evitar GC:
Uma coisa importante a ter em conta é que as variáveis globais não são limpas pelo garbage collector durante sobre a vida de sua página. Isso significa que objetos como as funções acima são criados apenas uma vez, então sempre que possível use-os para seu proveito. E variáveis globals são limpas quando os usuários atualizam a página, naveguam para outra página ou fecham a página.
Estas são maneiras direitas para evitar acessos de desempenho vindas do GC, mas você deve também estar ciente de outras funções da biblioteca de JavaScript que podem criar objetos. Ao saber que os valores são retornados de suas funções de biblioteca, você pode tomar melhores decisões sobre como criar seu código. Por exemplo, se você sabe que a função da biblioteca pode alocar memória e que fez uso da função em uma seção do seu código de desempenho crítico, você pode querer reescrever o código ou usar uma função semelhante, mas mais eficiente.
Engines de JavaScript fazem alguma preparação em seu código (incluindo algumas otimizações) antes da execução. Sabendo o que eles fazem nos bastidores irá
permitir que você escreva um código melhor. Aqui está uma visão geral de como duas engines de JavaScript populares (V8 do Google e SpiderMonkey da Mozilla) trabalham sob o capô:
- JavaScript é analisado e código de máquina nativo é gerado para uma execução mais rápida. O código inicial não é altamente otimizado.
- Um runtime profiler monitora o código que está sendo executado e detecta funções "quentes" (por exemplo, código rodou por muito tempo).
- O código que está marcado como "quente" será recompilado e otimizado.
- V8 pode desotimizar código anteriormente otimizado se descobre que alguns dos pressupostos que fez sobre o código otimizado foram muito otimistas.
- Objetos em V8 são representados com hidden classes para melhorar o acesso da propriedade.
- JavaScript é analisado e bytecode é gerado.
- O código que está marcado como "quente" será recompilado e otimizado pelo compilador Just-In-Time (JIT).
Como podem ver, ambas as engines e outras engines similares aplicam otimizações em comum das quais podemos tirar vantagem.
DELETANDO PROPRIEDADES DE OBJETO
Evite usar a palavra-chave delete para a remoção de propriedades do objeto, se puder. Considere o código a seguir:
Isso irá forçar V8 mudar a hidden class do objeto e executá-lo em um caminho de código mais lento. Mesmo é verdade sobre outras engines de JavaScript que otimizam objetos "quentes". Se possível, é melhor zerar as propriedades do objeto em vez de removê-los.
VARIÁVEIS MONOMÓRFICAS
Sempre que possível, tente manter a sua variável monomórfica. Por exemplo, não coloque objetos diferentes com diferentes classes escondidas em suas matrizes.
Mesmo se aplica a propriedades e parâmetros da função. As funções são que fornecidas com tipos de parâmetros constantes executam mais rápido do que os com diferentes parâmetros.
var arr = [1, "", {}, undefined, true];
A função abaixo pode ser chamada com diferentes tipos de parâmetros (ints, strings, objetos, etc), mas vai deixa-lá devagar:
ARRAY DE NUMEROS
Usar uma matriz normalmente é mais rápido do que acessar as propriedades do objeto. Isto é particularmente benéfico quando a matriz contém números. Por exemplo, é melhor escrever vetores utilizando matrizes do que com objetos com x, y, z propriedades.
ARRAYS COM BURACOS
Evite "buracos" em suas matrizes. Isso deixará tudo mais lento do que deveria. Buracos são criados apagando elementos ou adicionando elementos fora do alcance da matriz. Por exemplo:
var arr = [1, 2, 3, 4, 5];//Full array
delete arr[0];//Creates hole
arr[7] = 1;   //Creates hole
var hArr = [0, 1, 2, 3, /* hole */, 5];//Holey array
PRÉ-ALOCANDO GRANDES ARRAYS
Atuais implementações de SpiderMonkey e V8 favorecem o crescimento sobre grandes arrays pré-alocadas (com mais de 64k elementos). Tenha em mente que os membros deste é em grande parte dependente de implementação pois algumas implementações, tais como Nitro(Safari) ou Carakan(Opera) favorecem matrizes pré-alocadas.
DECLARAÇÃO DE OBJETOS
Eu não posso dar-lhe um método único para a criação de objetos como é muito dependente da engine, mas você pode ver os resultados de teste de desempenho atual aqui [http://jsperf.com/pr...performance/136] e decidir o que é melhor para sua aplicação.
INTEIRO ARITMÉTICO
Use números inteiros sempre que possível. Porque a maioria dos programas usam inteiros, motores de JavaScript modernas são otimizados para operações com números inteiros. Em JavaScript, todos os números são do tipo Number, então você não pode especificar diretamente tipo de armazenamento (int, float, double, etc) como outras "linguagens fortemente tipadas". Se seu aplicativo é matemática pesada, um involuntário ponto flutuante artimetico pode degradar o desempenho do aplicativo e ele pode se espalhar por meio de sua aplicação. Por exemplo:
v[0] /= 2;
v[1] /= 2;
v[2] /= 2;
var v = [3, 5, 9];
Em uma linguagem fortemente tipada como C++ onde usamos tipo int obteríamos [1, 2, 4], como resultado, mas no nosso caso, implicitamente mudamos para ponto flutuante em nosso código.
Engines de JavaScript usam as operações matemáticas inteiras sempre que possível e é porque processadores modernos executam operações com números inteiros mais rápido do que as operações de ponto flutuante. Ao contrário de outros objetos e valores de ponto flutuante, valores inteiros são armazenados na memória comum onde não necessitam de alocação.
Para dizer à engine de JavaScript que queremos armazenar valores inteiros em nossa matriz no exemplo acima poderíamos usar bitwise ou operador:
v[0] = (v[0] / 2) | 0;
v[1] = (v[1] / 2) | 0;
v[2] = (v[2] / 2) | 0;
Resultado do bitwise ou operador é um inteiro, desta forma a engine JavaScript sabe que não deve alocar memória.
VALORES DE PONTO FLUTUANTE
Conforme referido no ponto anterior, sempre que um número de ponto flutuante é atribuído à propriedade de objeto ou elemento da matriz, é uma memória alocada.
Se o seu programa faz um monte de atribuições deponto flutuante, a matemática destes pode ser caro. Embora não seja possível evitar a alocação para as propriedades do objeto, você pode usar arrays tipados(Float32Array e Float64Array).
Arrays tipados só podem armazenar valores de ponto flutuante, e o runtime do JavaScript pode acessar e armazenar os valores, sem alocações de memória.
Existem muitas outras maneiras de otimizar o seu código, mas eu acho que isso deve ser o suficiente para começar. Basta ter em mente que você sempre deve fazer o perfil de seu código e otimizar para as porções que leva mais tempo para executar.