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
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
Todo o código JavaScript deste aplicativo Web foi escrito como Módulos ES6
Tentei reescrever todas as funções usando a
function
sintaxe 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 for
loop aninhado .
Em relação à função na qual ele é executado, parece que um for
loop 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
mousemove
evento 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
juliaCalcJS
para 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
for
loop aninhado chama a funçãojuliaIter
para 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
makeJuliaXStepFn
emakeJuliaYStepFn
fora dofor
loop são passadas para asjuliaIter
quais, 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 juliaXStepFn
e juliaYStepFn
dentro dos for
loops 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 for
loop. No entanto, movendo as declarações de função dentro do for
loop, esse código é executado entre 2 e 3 vezes mais rápido!
Alguém pode explicar esse comportamento?
obrigado