JavaScript: Desempenho de forEach, map e reduce vs for e for...of
Ivan Trindade
Posted on January 16, 2023
Diversos desenvolvedores não sabem responder uma pergunta simples envolvendo o lopp while
, for
, etc. Muitos preferem escrever apenas código declarativo e acham mais sentido em programação imperativa. Embora concorde parcialmente com eles, isso me leva a pensar se os desenvolvedores sempre devem preferir os métodos array .map
, .reduce
e .forEach
em vez de loops simples em JavaScript.
O estilo de programação declarativa é muito expressivo, mais fácil de escrever e muito mais legível. É melhor 99% das vezes, mas não quando o desempenho é importante. Os loops geralmente são três ou mais vezes mais rápidos do que suas contrapartes declarativas.
Isso não representa uma diferença significativa na maioria das aplicações. Ainda assim, ao processar grandes quantidades de dados em alguma aplicação de inteligência de negócios, processamento de vídeo, cálculos científicos ou mecanismo de jogo, isso terá um efeito enorme no desempenho geral.
Neste artigo você verá alguns testes de desempenho com relação a esses métodos. Abaixo você verá o que abordaremos neste artigo:
- Sobre os testes
- Array.forEach vs for e for..of
- Array.map vs for vs for..of
- Array.reduce vs for e for..of
- Conclusão
Sobre os testes
A aplicação de teste usa a biblioteca de benchmark para obter resultados estatisticamente significativos.. A entrada para os testes foi um array de um milhão de objetos com a estrutura {a: number; b: number; r: number}
. Aqui está o código que gera esse array:
function generateTestArray() {
const result = [];
for (let i = 0; i < 1000000; ++i) {
result.push({
a: i,
b: i / 2,
r: 0,
});
}
return result;
}
Array.forEach vs for e for..of
Este teste calcula a soma de a
e b
para cada elemento do array e o armazena em r
:
array.forEach((x) => {
x.r = x.a + x.b;
});
Criei deliberadamente um campo r
no objeto durante a gravação do array, para evitar alterar a estrutura do objeto, pois isso afetará os benchmarks.
Mesmo com esses testes simples, os loops são quase três vezes mais rápidos. O loop for..of
está um pouco á frente do resto, mas a diferença não é significativa. A micro-otimização do loop for
que funciona em algumas outras linguagens, como armazenar em cache o tamanho do array ou armazenar um elemento para acesso repetido em uma variável temporária, teve efeito zero no JavaScript em execução no V8. Provavelmente o V8 já os faz sob o capô.
Como o loop .forEach não é diferente do loop for..of, não vemos mais sentido em usá-lo sobre o loop tradicional na maioria dos casos. Vale a pena usar apenas quando você já tiver uma função para invocar em cada elemento do array. Neste caso, é um one-liner, com degradação de desempenho zero:
array.forEach(func);
Array.map vs for vs for..of
Esse testes simples, mapeiam o array para outro array com o a
+ b
para cada elemento:
return array.map((x) => x.a + x.b);
Os loops também são muito mais rápidos aqui. O for..of
cria um array vazio e usa um push
em cada novo elemento:
const result = [];
for (const { a, b } of array) {
result.push(a + b);
}
return result;
Não é a abordagem ideal, pois o array é realocado dinamicamente e movido sob o capô. A versão for
pré-aloca o array com o tamanho de destino e define cada elemento usando o índice:
const result = new Array(array.length);
for (let i = 0; i < array.length; ++i) {
result[i] = array[i].a + array[i].b;
}
return result;
Aqui, também testamos se a desestruturação tem algum efeito no desempenho. Com o .map
, os benchmarks eram idênticos, e com for..of
, os resultados não são tão diferentes e pode ser apenas um acaso do benchmark.
Array.reduce vs for e for..of
Aqui, apenas calculamos a soma de a
e b
para todo o array:
return array.reduce((p, x) => p + x.a + x.b, 0);
Ambos for
e for..of
são 3,5 vezes mais rápidos que reduce. No entanto, os loops são muito mais detalhados:
let result = 0;
for (const { a, b } of array) {
result += a + b;
}
return result;
Escrever tantas linhas de código para apenas uma soma simples deve ter um motivo forte, portanto, a menos que o desempenho seja tão crítico, .reduce
é muito melhor. Os testes novamente não mostraram diferença entre os loops.
Conclusão
Os benchmarks provaram que a programação imperativa com loops, resulta em melhor desempenho do que o usuo de métodos Array convenientes. Invocar a função callback, não é gratuito e aumenta para grandes arrays. No entanto, para um código mais complexo do que uma soma simples, não haverá muita diferença relativa, pois os próprios cálculos levariam mais tempo.
O código imperativo é muito mais detalhado na maioria dos casos. Cinco linha de código para uma sompla simples, são demais e o .reduce
são apenas uma linha. Por outro lado, o .forEach
é quase o mesmo que for
ou for..of
, apenas mais lento. Não há muita diferença de desempenho entre os dois loops e você pode usar o que for mais adequado ao algoritmo.
Ao contrário do AssemblyScript, as microotimizações do loop for
, não fazem sentido para arrays em JavaScript. O V8 já faz um ótimo trabalho e provavelmente também elimina as verificações de limite.
A pré-alocação de um array de comprimento conhecido, é muito mais rápido do que a depender do crescimento dinâmico com push
. Também confirmamos que a desestruturação é gratuita e deve ser usada sempre que for conveniente.
Bons desenvolvedores devem saber como o código funciona e escolher a melhor solução em cada situação. A programação declarativa, com sua simplicidade, vence na maioria das vezes. Escrever código de nível ionferior, só faz sentido em dois casos:
- para otimizar os gargalos encontrados por perfis extensivos
- para código de desempenho obviamente crítico
Lembre-se, otimizações prematuras são a raiz de todos os males. Obrigado por ler e espero que tenha gostado do artigo.
Posted on January 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.