¿Alguien puede explicar este comportamiento inesperado de rendimiento de JavaScript V8?

8

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

  1. 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

  2. Todo el código JavaScript para esta aplicación web ha sido escrito como módulos ES6

  3. He intentado reescribir todas las funciones utilizando la functionsintaxis 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 forbucle anidado .

En relación con la función dentro de la cual se ejecuta, parece que un forbucle 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 mousemoveevento
  • 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 juliaCalcJSpara 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 forbucle anidado llama a la función juliaIterpara 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 makeJuliaXStepFny makeJuliaYStepFnfuera del forciclo se pasan a lo juliaIterque 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 juliaXStepFny juliaYStepFndentro de los forbucles 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 forciclo. Sin embargo, al mover las declaraciones de funciones dentro del forbucle, este código se ejecuta entre 2 y 3 veces más rápido.

¿Alguien puede explicar este comportamiento?

Gracias

Chris W
fuente
Preguntas (pueden ayudar, no sé): ¿Qué navegador (s) está utilizando? ¿La ganancia de rendimiento se nota solo en js o en webassembly también?
Calculuswhiz
1
Gracias @Calculuswhiz, esto parece ser un problema específico de Chrome / Brave. Safari y Firefox no parecen verse afectados. Actualizaré la publicación en consecuencia
Chris W
1
Este es un resumen muy detallado ... ¿alguna razón por la que ha archivado lo que es básicamente un boleto V8 en un sitio web de preguntas y respuestas de programación general, en lugar de en el rastreador de problemas V8 ?
Mike 'Pomax' Kamermans
2
Publicó un problema en el rastreador V8. Es el primero allí
Jeremy Gottfried
44
Supongo que tener todo dentro de la iteración simplifica el gráfico de dependencia para el optimizador, que luego puede producir un mejor código. Un generador de perfiles v8 podría arrojar algo más de luz sobre lo que está sucediendo.
rustyx

Respuestas:

1

Mi código logró caer de un precipicio de rendimiento conocido en el motor V8 JavaScript ...

Los detalles del problema y la solución se describen en bugs.chromium.org

Chris W
fuente