Não caia na armadilha da otimização prematura

Donald Knuth disse uma vez as palavras que mais tarde se tornaram famosas: “O verdadeiro problema é que os programadores, não onde eles precisam nem quando precisam, gastam muito tempo cuidando da eficiência. A otimização prematura é a raiz de todos os males (ou pelo menos a maioria deles) na programação. ”



O autor do material, cuja tradução estamos publicando hoje, quer falar sobre como ele caiu na armadilha da otimização prematura e como ele entendeu por sua própria experiência amarga que a otimização prematura é a raiz de todos os males.

Jogo GeoArena Online


Alguns anos atrás, eu trabalhei no jogo da Web GeoArena Online (então eu o vendi , os novos proprietários o publicaram em geoarena.io ). Era um jogo multiplayer no estilo do "último sobrevivente". Lá, o jogador controlava o navio, lutando um contra o outro.


Jogo GeoArena Online


Jogo GeoArena Online

Um jogo dinâmico, cujo mundo está cheio de partículas e efeitos, requer sérios recursos de computação. Como resultado, o jogo em alguns computadores antigos "desacelerou" em momentos particularmente tensos. Eu, um homem que não é indiferente a questões de produtividade, peguei a solução para esse problema com interesse. "Como acelerar a parte JavaScript do GeoArena no lado do cliente", perguntei-me.

Biblioteca Fast.js


Depois de pesquisar um pouco na Internet, descobri a biblioteca fast.js. Era uma "coleção de micro-otimizações destinadas a simplificar o desenvolvimento de programas JavaScript muito rápidos". Essa biblioteca foi acelerada pela disponibilidade de implementações mais rápidas dos métodos padrão internos, como Array.prototype.forEach () .

Achei isso extremamente interessante. O GeoArena usou muitas matrizes, executou muitas operações com matrizes, portanto, usar o fast.js poderia muito bem me ajudar a acelerar o jogo. Os seguintes resultados do estudo de desempenho forEach() foram incluídos no README para fast.js.

 Native .forEach() vs fast.forEach() (10 items)  ✓ Array::forEach() x 8,557,082 ops/sec ±0.37% (97 runs sampled)  ✓ fast.forEach() x 8,799,272 ops/sec ±0.41% (97 runs sampled)  Result: fast.js is 2.83% faster than Array::forEach(). 

Como um método implementado em alguma biblioteca externa pode ser mais rápido que sua versão padrão? O fato é que houve um truque (eles, esses truques, são encontrados em todos os lugares que você olha). A biblioteca era adequada apenas para trabalhar com matrizes que não eram escassas.

Aqui estão alguns exemplos simples de tais matrizes:

 //  -  :   1  . const sparse1 = [0, , 1]; console.log(sparse1.length); // 3 //  -   const sparse2 = []; // ...   - .   0 - 4    . sparse2[5] = 0; console.log(sparse2.length); // 6 

Para entender por que a biblioteca não pode funcionar normalmente com matrizes esparsas, examinei seu código-fonte. Descobriu-se que a implementação forEach() no fast.js se baseia em loops. Uma implementação rápida do método forEach() seria algo como isto:

 //     . function fastForEach(array, f) {  for (let i = 0; i < array.length; i++) {    f(array[i], i, array);  } } const sparseArray = [1, , 2]; const print = x => console.log(x); fastForEach(sparseArray, print); //  print() 3 . sparseArray.forEach(print); //  print()  2 . 

Uma chamada para o método fastForEach() três valores:

 1 undefined 2 

Chamar sparseArray.forEach() leva apenas à conclusão de dois valores:

 1 2 

Essa diferença se deve ao fato de que as especificações JS referentes ao uso de funções de retorno de chamada indicam que essas funções não devem ser chamadas em índices de matriz remotos ou não inicializados (também chamados de "brechas"). A implementação fastForEach() não verificou a matriz quanto a fastForEach() . Isso levou a um aumento na velocidade com o custo do trabalho correto com matrizes esparsas. Isso foi perfeito para mim, pois matrizes esparsas não foram usadas no GeoArena.

Neste ponto, eu deveria apenas fazer um teste rápido no fast.js. Devo instalar a biblioteca, alterar os métodos padrão do objeto Array para métodos de fast.js e testar o desempenho do jogo. Mas, em vez disso, mudei para uma direção completamente diferente.

Meu desenvolvimento chamado mais rápido.js


O perfeccionista maníaco que vive em mim queria espremer absolutamente tudo da otimização do desempenho do jogo. A biblioteca fast.js simplesmente não me pareceu uma solução boa o suficiente, pois seu uso implicava chamar seus métodos. Então pensei: “E se eu substituir os métodos padrão de matrizes simplesmente incorporando implementações novas e mais rápidas desses métodos no código? Isso me pouparia a necessidade de chamadas de método de biblioteca. ”

Foi essa ideia que me levou à idéia engenhosa, que era criar um compilador, que eu descaradamente chamei de fast.js. Planejei usá-lo em vez do fast.js. Por exemplo, aqui está o trecho de código-fonte:

 //   const arr = [1, 2, 3]; const results = arr.map(e => 2 * e); 

O compilador Fast.js converteria esse código para o seguinte - mais rápido, mas com uma aparência pior:

 //      faster.js const arr = [1, 2, 3]; const results = new Array(arr.length); const _f = (e => 2 * e); for (let _i = 0; _i < arr.length; _i++) {  results[_i] = _f(arr[_i], _i, arr); } 

A criação do Fast.js foi motivada pela mesma idéia que sustentava o Fast.js. Ou seja, estamos falando de micro otimizações de desempenho devido à rejeição do suporte a matrizes esparsas.

À primeira vista, o Fast.js me pareceu um desenvolvimento extremamente bem-sucedido. Aqui estão alguns resultados de um estudo de desempenho do Fast.js:

   array-filter large    ✓ native x 232,063 ops/sec ±0.36% (58 runs sampled)    ✓ faster.js x 1,083,695 ops/sec ±0.58% (57 runs sampled) faster.js is 367.0% faster (3.386μs) than native  array-map large    ✓ native x 223,896 ops/sec ±1.10% (58 runs sampled)    ✓ faster.js x 1,726,376 ops/sec ±1.13% (60 runs sampled) faster.js is 671.1% faster (3.887μs) than native  array-reduce large    ✓ native x 268,919 ops/sec ±0.41% (57 runs sampled)    ✓ faster.js x 1,621,540 ops/sec ±0.80% (57 runs sampled) faster.js is 503.0% faster (3.102μs) than native  array-reduceRight large    ✓ native x 68,671 ops/sec ±0.92% (53 runs sampled)    ✓ faster.js x 1,571,918 ops/sec ±1.16% (57 runs sampled) faster.js is 2189.1% faster (13.926μs) than native 

Os resultados completos dos testes podem ser encontrados aqui . Eles foram realizados no Node v8.16.1, no MacBook Pro de 15 polegadas 2018.

Meu desenvolvimento é 2000% mais rápido que a implementação padrão? Um aumento tão sério na produtividade é, sem dúvida, algo que pode ter o maior impacto positivo em qualquer programa. Certo?
Não, não é verdade.

Considere um exemplo simples.

  • Imagine que o jogo GeoArena médio requer 5.000 milissegundos (ms) de computação.
  • O compilador Fast.js acelera a execução dos métodos de matriz em uma média de 10 vezes (esta é uma estimativa aproximada, além disso, está superestimada; na maioria das aplicações reais, não há nem mesmo aceleração dupla).

E aqui está a pergunta que realmente nos interessa: “Qual parte desses 5000 ms é gasta na implementação de métodos de matriz?”.

Suponha metade. Ou seja, 2500 ms são gastos em métodos de matriz, os restantes 2500 ms em todo o resto. Nesse caso, o uso do acceler.js proporcionará um enorme aumento de desempenho.


Exemplo condicional: o tempo de execução do programa é muito reduzido

Como resultado, verifica-se que o tempo computacional total foi reduzido em 45%.

Infelizmente, todos esses argumentos estão muito, muito longe da realidade. O GeoArena, é claro, usa muitos métodos de matriz. Mas a distribuição real do tempo de execução do código para diferentes tarefas é semelhante à seguinte.


Realidade dura

Infelizmente, o que posso dizer.

Este é exatamente o erro que Donald Knuth alertou. Não coloquei meus esforços no que eles deveriam ser aplicados e não fiz quando valeu a pena.

Aqui a matemática simples entra em jogo. Se algo consumir apenas 1% do tempo de execução do programa, a otimização proporcionará, na melhor das hipóteses, apenas um aumento de 1% na produtividade.

Isso é exatamente o que Donald Knuth tinha em mente quando disse "não onde é necessário". E se você pensar sobre o que "onde você precisa", verifica-se que essas são as partes dos programas que representam os gargalos de desempenho. Esses são os trechos de código que contribuem significativamente para o desempenho geral do programa. Aqui, o conceito de "produtividade" é usado em um sentido muito amplo. Pode incluir o tempo de execução do programa, o tamanho do seu código compilado e outra coisa. Uma melhoria de 10% nessa parte do programa que afeta muito o desempenho é melhor do que uma melhoria de 100% em algo pequeno.

Knut também falou da aplicação de esforços "não quando necessário". O ponto disso é que você precisa otimizar algo apenas quando necessário. Claro, eu tinha um bom motivo para pensar em otimização. Mas lembre-se de que comecei a desenvolver o fast.js e, antes disso, nem tentei testar a biblioteca fast.js no GeoArena? Minutos gastos testando fast.js no meu jogo me poupariam semanas de trabalho. Espero que você não caia na mesma armadilha em que caí.

Sumário


Se você estiver interessado em experimentar o Fast.js, pode dar uma olhada nesta demonstração. Os resultados obtidos dependem do seu dispositivo e navegador. Aqui, por exemplo, o que aconteceu no Chrome 76 no MacBook Pro de 15 polegadas 2018.


Resultados do teste Faster.js

Você pode estar interessado em aprender sobre os resultados reais do uso do acceler.js no GeoArena. Eu, quando o jogo ainda era meu (como eu disse, eu o vendi), conduzi algumas pesquisas básicas. Como resultado, resultou o seguinte:

  • O uso do accelerator.js acelera a execução do ciclo principal do jogo em um jogo típico em cerca de 1%.
  • Devido ao uso do Fast.js, o tamanho do pacote do jogo aumentou 0,3%. Isso diminuiu um pouco o carregamento da página do jogo. O tamanho do pacote aumentou devido ao fato de o Fast.js converter o código curto padrão em um código mais rápido, mas também mais longo.

Em geral, o Fast.js tem seus prós e contras, mas esse desenvolvimento não teve muito impacto no desempenho do GeoArena. Eu teria entendido isso muito antes se tivesse me incomodado em testar o jogo primeiro usando o fast.js.

Que minha história sirva como um aviso para você.

Caros leitores! Você caiu na armadilha da otimização prematura?

Source: https://habr.com/ru/post/pt465809/


All Articles