Executando verificação de segurança...
5

[Javascript] o map() mais performatico que for...of.

Em dado momento da minha profissinal de programador, vi uma materia afirmando e comprovando que um for...of é mais performatico que um map(), e se eu te diser que da pra fazer um map() ser mais performatico que um for...of, você sabe me dizer como? Deixa eu te contar uma historia.

Hoje resolvendo minha task de cada dia, me deparei com um codigo de outro dev que a principio achei bizarro, aparentemente era nescessario rodar map() dentro de map() dentro de map(), ate então ok, não queria mexer ali e talvez encontrar um boss lvl99, mas olhando um pouco mais o estranho era que nem todo map() era assincrono e mesmo assim ele usava await Promise.all([].map()), pensando totalmente dentro da caixa critiquei pra caramba e refatorei usando for...of em seguida fiz uma teste usando console.time() & console.timeEnd() obtendo uma media de ~250ms, orgulhoso do resultado guardei o codigo refatorado e rodei varios ctrl + Z para voltar ao codigo original e fazer o mesmo teste de performace e me surpreendi com o um resultado melhor de ~130ms, totalmente descrente, achei que tinha feito algo de errado em minha refatoração mas saindo da caixinha e olhando para o lado teórico do assunto, sabemos que o Promise.all() vai executar cada loop de um map em simultaneo, diferente do for...of que irá executar um loop após o termino do outro, dessa forma, nosso map() acaba sendo mais performatico que o for...of.

Quem diria em amigos, a gente cria nossos proprios padroes de codigo e as vezes olhamos para outras formas de resolver e achamos a coisa mais bizarra do mundo, afinal por que bulhunfas eu iria usar um Promise.all() para uma coisa nao assincrona rsrsrs, ja pensou se eu simplesmente refatoro sem comprovar os resultados teria dado um downgrade no metodo rsrsrs.

Oque acharam? pega leve é meu primeiro post.

Carregando publicação patrocinada...
1

O post ficou incrível!

Falou de uma experiência real de algo comum, tambem já sofri com isso kkkk

Uma coisa é que você consegue usar o Promise.all() com um .map() e não com um .filter() aparentemente, levei um bom tempo pra descobrir kkkkkk

Só senti um pouco de falta em exemplos de código e formatação no texto, tirando isso, o conteúdo ta fantástico!

1

Vc pode compartilhar o código completo? Ou pelo menos algum exemplo reduzido que dê o mesmo resultado. Porque sem isso não tem como analisar o que está fazendo com que um seja mais rápido que outro, nem o que de fato está sendo comparado.

Em geral, um for costuma ser mais rápido que um map/forEach/filter/reduce, já que estes métodos recebem uma função de callback que é executada para cada elemento (embora a diferença seja imperceptível para arrays pequenos, ainda sim tem o custo de chamar a função várias vezes, que dependendo do caso não pode ser desprezado). Mas cada caso é um caso, e se entram operações assíncronas no meio, tudo pode mudar dependendo do que é feito e como as operações são executadas.

Vale lembrar também que console.time não é a maneira mais confiável de comparar tempos de execução, como já explicado aqui.

1

Olá k, até posso compartilhar mas o trecho nao fará sentido algum sem todo o contexto que ele se encontra, aqui está a parte que mensurei no post, foi por volta desse promise all:

await Promise.all(
      proposals.map(async (proposal) => {
        // Taxa
        if (proposal.proposalTax.length) {
          chargeIds.push({
            pagarMeChargeId: proposal.proposalTax[0].pagarMeChargeId,
            pagarMeUsed: proposal.proposalTax[0].pagarMeUsed,
          })
        }

        // PropostaPayments
        const proposalPayments = await ProposalPayments.query()
          .preload('proposalBoletoParcelado')
          .where('proposal_id', proposal.$attributes.id)
          .andWhereNotNull('pagar_me_order_id')

        if (proposalPayments && proposalPayments.length) {
          await Promise.all(
            proposalPayments.map(async (pp) => {
              // Boleto Parcelado
              if (pp.proposalBoletoParcelado && pp.proposalBoletoParcelado.length) {
                await Promise.all(
                  pp.proposalBoletoParcelado.map((proposalBoletoParcelado) => {
                    chargeIds.push({
                      pagarMeChargeId: proposalBoletoParcelado.pagarMeChargeId,
                      pagarMeUsed: proposalBoletoParcelado.pagarMeUsed,
                    })
                  })
                )
              }

              chargeIds.push({
                pagarMeChargeId: pp.pagarMeChargeId!,
                pagarMeUsed: proposal.pagarMeUsed,
              })
            })
          )
        }

        // PropostaPayments
        const proposalCardMemberships = await ProposalCardMemberships.query()
          .preload('proposalCardCycles')
          .where('proposal_id', proposal.$attributes.id)

        if (proposalCardMemberships && proposalCardMemberships.length) {
          await Promise.all(
            proposalCardMemberships.map(async (pcm) => {
              // Ciclos
              if (pcm.proposalCardCycles && pcm.proposalCardCycles.length) {
                await Promise.all(
                  pcm.proposalCardCycles.map((pcc) => {
                    if (!pcc.pagarMeChargeId) return
                    chargeIds.push({
                      pagarMeChargeId: pcc.pagarMeChargeId,
                      pagarMeUsed: pcm.pagarMeUsed,
                    })
                  })
                )
              }
            })
          )
        }
      })
    )

Estou esperando um tempo livre para tentar reproduzi-lo e em breve posto aqui minhas conclusoes.

de cara no ultimo teste que fiz, tive resultados negativos quanto ao promisse.all().

import { createServer } from 'node:http'
import { performance } from 'node:perf_hooks'

const app = createServer()
app.on("request", async (_, response) => {
  const casas = Array(100).fill({
    pessoas: Array(100).fill({
      bicicletas: Array(100).fill({
        cor: ""
      })
    })
  })

  const map = () => {
    let list = []
    performance.mark("mapStart")
    casas.map(casa => casa.pessoas.map(pessoa => pessoa.bicicletas.map(bicicleta => list.push(bicicleta.cor))))
    performance.mark("mapEnd")
    performance.measure("map", "mapStart", "mapEnd")
  }

  const forof = () => {
    let list = []
    performance.mark("forofStart")
    for (const casa of casas) {
      for (const pessoa of casa.pessoas) {
        for (const bicicleta of pessoa.bicicletas) {
          list.push(bicicleta.cor)
        }
      }
    }
    performance.mark("forofEnd")
    performance.measure("forof", "forofStart", "forofEnd")
  }

  const all = async () => {
    let list = []
    performance.mark("allStart")
    await Promise.all(
      casas.map(async casa => {
        await Promise.all(
          casa.pessoas.map(async pessoa => {
            await Promise.all(
              pessoa.bicicletas.map(bicicleta => list.push(bicicleta.cor))
            )
          })
        )
      })
    )
    performance.mark("allEnd")
    performance.measure("all", "allStart", "allEnd")
  }

  map()
  forof()
  await all()
  console.log(performance.getEntriesByType("measure"))
  response.end("ok")
})
app.listen(3333)

resultados:

[
PerformanceMeasure {
name: 'map',
entryType: 'measure',
startTime: 3310.392207,
duration: 33.59345100000019
},
PerformanceMeasure {
name: 'forof',
entryType: 'measure',
startTime: 3344.32724,
duration: 29.174007000000074
},
PerformanceMeasure {
name: 'all',
entryType: 'measure',
startTime: 3373.629408,
duration: 302.93898400000035
}
]

2

Bom, mudando um pouco de assunto, uma dica que posso dar é que - pelo menos no último exemplo - vc está não está usando map da melhor maneira. Quer dizer, até "funciona", mas não é a ferramenta adequada para este caso.

A ideia do map é executar algo com cada elemento de um array, e retornar outro array com os resultados. Ou seja, array.map(função) executa a função para cada elemento do array, e cria outro array com os retornos desta função. Mas no seu caso vc está ignorando estes arrays retornados, que estão sendo criados à toa.

No fundo, vc está usando map como um substituto do for. E isso até "funciona", já que no fim ele vai percorrer por todos os elementos. Mas se vc não quer os resultados (os arrays que são criados cada vez que vc chama map), então é um uso torto.

Vale lembrar que o resultado do map é um array com o mesmo tamanho do array original, mas também não é isso que vc quer. Vc tem o array casas com 100 elementos. Mas cada um desses elementos possui outro array, que por sua vez tem 100 elementos, sendo que cada um deles tem mais 100 elementos cada. E no fim o que te interessa é o array final com 1 milhão de elementos (os valores de todas as cores). Só o fato de resultar em um array com tamanho diferente do original já indica que map não é o mais adequado.

Enfim, neste caso o ideal é usar for mesmo. Inclusive, fiz uns testes usando o Benchmark.js e o for foi bem mais rápido que o map.


Quanto a usar Promise.all, não faz sentido ali porque este método deve receber um array de promises. Mas vc está passando um array de arrays de arrays de números (sim, pois push retorna o tamanho do array após a inserção do elemento, e depois este array é retornado para o map de fora, que por sua vez repassa como um array de arrays para o map mais externo). E segundo a documentação, os elementos que não são promises são ignorados. De qualquer forma, a demora talvez ocorra porque há um overhead em criar as promises - mas não muda o fato de não fazer sentido usar promises neste caso.


Quanto ao primeiro código, realmente fica difícil avaliar sem todo o contexto. Ainda sim eu acho estranho usar map ali, pois no fundo parece que está fazendo array.map(e => outroArray.push(e)) - ou seja, está usando map como um loop para preencher outro array, o que eu já disse que é um uso torto. E usar isso em conjunto com Promise.all cai no que também citei acima, de passar coisas que não são promises, o que não faz sentido.

Enfim, eu revisaria esses pontos antes de tirar mais conclusões. Não sei nem se o uso de map que deixou mais rápido, meu palpite é que não (mas claro, é apenas um chute sem conhecer todo o contexto).

1
1

Esse código ta errado, o Promise.all recebe um Array de Promise, para ficar certo seria algo tipo:

Promise.all(data.map(async (item) => item.doSomething()))

Aqui o map vai gerar o array que o Promise.all precisa usando uma função assíncrona

1
1

Creio que não tenha diferença, contanto que as Promises estejam sendo guardadas para depois aguardar o fim delas no Promise.all

Do jeito que ta ele vai rodar lento já que as tarefas dentro do for..of provavelmente não são executadas de forma assíncrona

1

O map retorna um array pra dentro do Promise.all().
O map não espera nada nem ninguém, vai lá e faz e retona o array pra dentro do
Promise.all().

const arr = [].map()
Promise.all(arr)

Daria quase na mesma claro que ter uma variavel muda a velocidade!

For of é mais lento por usar iteradores por debaixo dos panos e toda a sobrecarga que vem com eles.

https://chromium.googlesource.com/v8/v8.git/+/3.31.25/src/array-iterator.js?autodive=0%2F%2F

Quer rapidez use o for velho de guerra!
for (let i = 0; i < arr.length; i++)

1

Se a função que ta no map for assíncrona o resultado dele vai ser um monte de Promise, dai da pra usar o Promise.all e gastar menos tempo, já fiz isso pra reduzir tempo de processamento e resposta em projetos pessoais(em um app e em uma API)