Alguém pode explicar esse comportamento inesperado do desempenho do V8 JavaScript?

8

Atualização (2 de março de 2020)

Acontece que a codificação no meu exemplo aqui foi estruturada da maneira certa para cair de uma falha de desempenho conhecida no mecanismo JavaScript V8 ...

Veja a discussão em bugs.chromium.org para obter detalhes. Este bug está sendo trabalhado e deve ser corrigido em um futuro próximo.

Atualização (9 de janeiro de 2020)

Tentei isolar a codificação que se comporta da maneira descrita abaixo em um aplicativo Web de página única, mas, ao fazer isso, o comportamento desapareceu (??). No entanto, o comportamento descrito abaixo ainda existe no contexto do aplicativo completo.

Dito isso, desde então, otimizei a codificação do cálculo fractal e esse problema não é mais um problema na versão ao vivo. Se alguém estiver interessado, o módulo JavaScript que manifesta esse problema ainda está disponível aqui

Visão geral

Acabei de concluir um pequeno aplicativo baseado na Web para comparar o desempenho do JavaScript baseado em navegador com o Web Assembly. Este aplicativo calcula uma imagem do conjunto Mandelbrot e, à medida que você move o ponteiro do mouse sobre essa imagem, o conjunto Julia correspondente é calculado dinamicamente e o tempo de cálculo é exibido.

Você pode alternar entre o JavaScript (pressione 'j') ou o WebAssembly (pressione 'w') para executar o cálculo e comparar os tempos de execução.

Clique aqui para ver o aplicativo em funcionamento

No entanto, ao escrever este código, descobri um comportamento de desempenho JavaScript inesperadamente estranho ...

Resumo do Problema

  1. Esse problema parece ser específico ao mecanismo JavaScript V8 usado no Chrome e no Brave. Esse problema não aparece nos navegadores que utilizam o SpiderMonkey (Firefox) ou JavaScriptCore (Safari). Não pude testar isso em um navegador usando o mecanismo Chakra

  2. Todo o código JavaScript deste aplicativo Web foi escrito como Módulos ES6

  3. Tentei reescrever todas as funções usando a functionsintaxe tradicional, em vez da nova sintaxe de seta ES6. Infelizmente, isso não faz nenhuma diferença significativa

O problema de desempenho parece estar relacionado ao escopo dentro do qual uma função JavaScript é criada. Neste aplicativo, chamo duas funções parciais, cada uma das quais me devolve outra função. Em seguida, passo essas funções geradas como argumentos para outra função chamada dentro de um forloop aninhado .

Em relação à função na qual ele é executado, parece que um forloop cria algo semelhante ao seu próprio escopo (embora não seja certo que seja um escopo completo). Em seguida, passar funções geradas por esse limite de escopo (?) É caro.

Estrutura básica de codificação

Cada função parcial recebe o valor X ou Y da posição do ponteiro do mouse sobre a imagem do Conjunto Mandelbrot e retorna a função a ser iterada ao calcular o conjunto Julia correspondente:

const makeJuliaXStepFn = mandelXCoord => (x, y) => mandelXCoord + diffOfSquares(x, y)
const makeJuliaYStepFn = mandelYCoord => (x, y) => mandelYCoord + (2 * x * y)

Essas funções são chamadas dentro da seguinte lógica:

  • O usuário move o ponteiro do mouse sobre a imagem do Mandelbrot Set acionando o mousemoveevento
  • A localização atual do ponteiro do mouse é convertida no espaço de coordenadas do conjunto de Mandelbrot e as coordenadas (X, Y) são passadas para funcionar juliaCalcJSpara calcular o conjunto de Julia correspondente.

  • Ao criar um conjunto Julia específico, as duas funções parciais acima são chamadas para gerar as funções a serem iteradas ao criar o conjunto Julia

  • Um forloop aninhado chama a função juliaIterpara calcular a cor de cada pixel no conjunto Julia. A codificação completa pode ser vista aqui , mas a lógica essencial é a seguinte:

    const juliaCalcJS =
      (cvs, juliaSpace) => {
        // Snip - initialise canvas and create a new image array
    
        // Generate functions for calculating the current Julia Set
        let juliaXStepFn = makeJuliaXStepFn(juliaSpace.mandelXCoord)
        let juliaYStepFn = makeJuliaYStepFn(juliaSpace.mandelYCoord)
    
        // For each pixel in the canvas...
        for (let iy = 0; iy < cvs.height; ++iy) {
          for (let ix = 0; ix < cvs.width; ++ix) {
            // Translate pixel values to coordinate space of Julia Set
            let x_coord = juliaSpace.xMin + (juliaSpace.xMax - juliaSpace.xMin) * ix / (cvs.width - 1)
            let y_coord = juliaSpace.yMin + (juliaSpace.yMax - juliaSpace.yMin) * iy / (cvs.height - 1)
    
            // Calculate colour of the current pixel
            let thisColour = juliaIter(x_coord, y_coord, juliaXStepFn, juliaYStepFn)
    
            // Snip - Write pixel value to image array
          }
        }
    
        // Snip - write image array to canvas
      }
  • Como você pode ver, as funções retornadas chamando makeJuliaXStepFne makeJuliaYStepFnfora do forloop são passadas para as juliaIterquais, então, faz todo o trabalho duro de calcular a cor do pixel atual

Quando olhei para essa estrutura de código, primeiro pensei: "Tudo bem, tudo funciona bem; então, não há nada errado aqui".

Exceto que havia. O desempenho foi muito mais lento que o esperado ...

Solução Inesperada

Muita cabeça coçando e mexendo ao redor seguiu ...

Depois de um tempo, descobri que, se movo a criação de funções juliaXStepFne juliaYStepFndentro dos forloops externos ou internos , o desempenho melhora por um fator entre 2 e 3 ...

WHAAAAAAT !?

Então, o código agora se parece com isso

const juliaCalcJS =
  (cvs, juliaSpace) => {
    // Snip - initialise canvas and create a new image array

    // For each pixel in the canvas...
    for (let iy = 0; iy < cvs.height; ++iy) {
      // Generate functions for calculating the current Julia Set
      let juliaXStepFn = makeJuliaXStepFn(juliaSpace.mandelXCoord)
      let juliaYStepFn = makeJuliaYStepFn(juliaSpace.mandelYCoord)

      for (let ix = 0; ix < cvs.width; ++ix) {
        // Translate pixel values to coordinate space of Julia Set
        let x_coord = juliaSpace.xMin + (juliaSpace.xMax - juliaSpace.xMin) * ix / (cvs.width - 1)
        let y_coord = juliaSpace.yMin + (juliaSpace.yMax - juliaSpace.yMin) * iy / (cvs.height - 1)

        // Calculate colour of the current pixel
        let thisColour = juliaIter(x_coord, y_coord, juliaXStepFn, juliaYStepFn)

        // Snip - Write pixel value to image array
      }
    }

    // Snip - write image array to canvas
  }

Eu esperaria que essa mudança aparentemente insignificante fosse um pouco menos eficiente, porque um par de funções que não precisam ser alteradas está sendo recriado cada vez que iteramos o forloop. No entanto, movendo as declarações de função dentro do forloop, esse código é executado entre 2 e 3 vezes mais rápido!

Alguém pode explicar esse comportamento?

obrigado

Chris W
fonte
Perguntas (posso ajudar, não sei): Quais navegadores você está usando? O ganho de desempenho é perceptível em apenas js ou em montagem na web também?
Calculuswhiz
1
Obrigado, @Calculuswhiz, este parece ser um problema específico do Chrome / Brave. Safari e Firefox não parecem ser afetados. Vou atualizar o post de acordo
Chris W
1
Este é um resumo muito detalhado ... qualquer motivo para você ter registrado o que é basicamente um ticket V8 em um site de perguntas e respostas sobre programação geral, e não no rastreador de problemas V8 ?
Mike 'Pomax' Kamermans
2
Ele postou um problema no rastreador V8. É o primeiro lá
Jeremy Gottfried
4
Meu palpite é que ter tudo dentro da iteração simplifica o gráfico de dependência do otimizador, que é capaz de produzir um código melhor. Um criador de perfil v8 pode lançar mais alguma luz sobre o que está acontecendo.
rustyx 6/01

Respostas:

1

Meu código conseguiu cair de um penhasco de desempenho conhecido no mecanismo JavaScript V8 ...

Os detalhes do problema e a correção estão descritos em bugs.chromium.org

Chris W
fonte