Actualización (2 de marzo de 2020)
Resulta que la codificación en mi ejemplo aquí fue estructurada de la manera correcta para caer de un precipicio de rendimiento conocido en el motor V8 JavaScript ...
Vea la discusión en bugs.chromium.org para los detalles. Ahora se está trabajando en este error y se debe solucionar en un futuro próximo.
Actualización (9 de enero de 2020)
Traté de aislar la codificación que se comporta de la manera descrita a continuación en una aplicación web de una sola página, pero al hacerlo, el comportamiento desapareció (??). Sin embargo, el comportamiento descrito a continuación todavía existe en el contexto de la aplicación completa.
Dicho esto, desde entonces he optimizado la codificación de cálculo fractal y este problema ya no es un problema en la versión en vivo. Si alguien está interesado, el módulo JavaScript que manifiesta este problema todavía está disponible aquí
Visión general
Acabo de completar una pequeña aplicación basada en web para comparar el rendimiento de JavaScript basado en navegador con Web Assembly. Esta aplicación calcula una imagen del conjunto de Mandelbrot, luego, al mover el puntero del mouse sobre esa imagen, el conjunto de Julia correspondiente se calcula dinámicamente y se muestra el tiempo de cálculo.
Puede cambiar entre usar JavaScript (presione 'j') o WebAssembly (presione 'w') para realizar el cálculo y comparar tiempos de ejecución.
Haga clic aquí para ver la aplicación que funciona.
Sin embargo, al escribir este código, descubrí un comportamiento inesperadamente extraño de rendimiento de JavaScript ...
Resumen del problema
Este problema parece ser específico del motor V8 JavaScript utilizado en Chrome y Brave. Este problema no aparece en los navegadores que usan SpiderMonkey (Firefox) o JavaScriptCore (Safari). No he podido probar esto en un navegador usando el motor Chakra
Todo el código JavaScript para esta aplicación web ha sido escrito como módulos ES6
He intentado reescribir todas las funciones utilizando la
function
sintaxis tradicional en lugar de la nueva sintaxis de flecha ES6. Desafortunadamente, esto no hace ninguna diferencia apreciable
El problema de rendimiento parece estar relacionado con el alcance dentro del cual se crea una función de JavaScript. En esta aplicación, llamo dos funciones parciales, cada una de las cuales me devuelve otra función. Luego paso estas funciones generadas como argumentos a otra función que se llama dentro de un for
bucle anidado .
En relación con la función dentro de la cual se ejecuta, parece que un for
bucle crea algo parecido a su propio alcance (aunque no estoy seguro de que sea un alcance completo). Entonces, pasar funciones generadas a través de este límite de alcance (?) Es costoso.
Estructura básica de codificación
Cada función parcial recibe el valor X o Y de la posición del puntero del mouse sobre la imagen del conjunto de Mandelbrot, y devuelve la función que se iterará al calcular el conjunto de Julia correspondiente:
const makeJuliaXStepFn = mandelXCoord => (x, y) => mandelXCoord + diffOfSquares(x, y)
const makeJuliaYStepFn = mandelYCoord => (x, y) => mandelYCoord + (2 * x * y)
Estas funciones se llaman dentro de la siguiente lógica:
- El usuario mueve el puntero del mouse sobre la imagen del conjunto de Mandelbrot que desencadena el
mousemove
evento La ubicación actual del puntero del mouse se traduce al espacio de coordenadas del conjunto de Mandelbrot y las coordenadas (X, Y) se pasan a la función
juliaCalcJS
para calcular el conjunto de Julia correspondiente.Al crear cualquier conjunto de Julia en particular, las dos funciones parciales anteriores se llaman para generar las funciones que se iterarán al crear el conjunto de Julia
Un
for
bucle anidado llama a la funciónjuliaIter
para calcular el color de cada píxel en el conjunto de Julia. La codificación completa se puede ver aquí , pero la lógica esencial es la siguiente: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 puede ver, las funciones devueltas al llamar
makeJuliaXStepFn
ymakeJuliaYStepFn
fuera delfor
ciclo se pasan a lojuliaIter
que luego hace todo el trabajo duro de calcular el color del píxel actual
Cuando miré esta estructura de código, al principio pensé "Esto está bien, todo funciona bien; así que no pasa nada aquí"
Excepto que hubo. El rendimiento fue mucho más lento de lo esperado ...
Solución inesperada
Le siguieron muchos rascarse la cabeza y juguetear ...
Después de un tiempo, descubrí que si muevo la creación de funciones juliaXStepFn
y juliaYStepFn
dentro de los for
bucles externo o interno , el rendimiento mejora en un factor de entre 2 y 3 ...
WHAAAAAAT !?
Entonces, el código ahora se ve así
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
}
Hubiera esperado que este cambio aparentemente insignificante fuera algo menos eficiente, porque un par de funciones que no necesitan cambiar se recrean cada vez que iteramos el for
ciclo. Sin embargo, al mover las declaraciones de funciones dentro del for
bucle, este código se ejecuta entre 2 y 3 veces más rápido.
¿Alguien puede explicar este comportamiento?
Gracias