这意外的V8 JavaScript性能行为有人可以解释吗?

huangapple go评论78阅读模式
英文:

Can anyone explain this unexpected V8 JavaScript performance behaviour?

问题

以下是您要翻译的内容:

Update (Mar 2nd, 2020)

It turns out that the coding in my example here was structured in just the right way to fall off a known performance cliff in the V8 JavaScript engine...

See the discussion over on bugs.chromium.org for the details. This bug is now being worked on and should be fixed in the near future.

Update (Jan 9th, 2020)

I tried to isolate the coding that behaves in the manner described below into a single page Web app, but in doing so, the behaviour disappeared(??). However, the behaviour described below does still exist in the context of the full application.

That said, I have since optimised the fractal calculation coding and this problem is no longer an issue in the live version. Should anyone be interested, the JavaScript module that manifests this problem is still available here

Overview

I've just completed a small Web-based app to compare the performance of browser-based JavaScript with Web Assembly. This app calculates a Mandelbrot Set image, then as you move the mouse pointer over that image, the corresponding Julia Set is dynamically calculated and the calculation time is displayed.

You can switch between using JavaScript (press 'j') or WebAssembly (press 'w') to perform the calculation and compare runtimes.

Click here to see the working app

However, in writing this code, I discovered some unexpectedly strange JavaScript performance behaviour...

Problem Summary

  1. This problem seems to be specific to the V8 JavaScript engine used in Chrome and Brave. This problem does not appear in browsers using SpiderMonkey (Firefox) or JavaScriptCore (Safari). I have not been able to test this in a browser using the Chakra engine

  2. All the JavaScript code for this Web app has been written as ES6 Modules

  3. I've tried rewriting all the functions using the traditional function syntax rather than the new ES6 arrow syntax. Unfortunately, this does not make any appreciable difference

The performance problem seems to relate to the scope within which a JavaScript function is created. In this app, I call two partial functions, each of which gives me back another function. I then pass these generated functions as arguments to another function that is called inside a nested for loop.

Relative to the function within which it executes, it appears that a for loop creates something resembling its own scope (not sure its a full-blown scope though). Then, passing generated functions across this scope(?) boundary is expensive.

Basic Coding Structure

Each partial function receives the X or Y value of the mouse pointer's position over the Mandelbrot Set image, and returns the function to be iterated when calculating the corresponding Julia set:

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

These functions are called within the following logic:

  • The user moves the mouse pointer over the image of the Mandelbrot Set triggering the mousemove event

  • The current location of the mouse pointer is translated to the coordinate space of Mandelbrot set and the (X,Y) coordinates are passed to function juliaCalcJS to calculate the corresponding Julia Set.

  • When creating any particular Julia Set, the above two partial functions are called to generate the functions to be iterated when creating the Julia Set

  • A nested for loop then calls function juliaIter to calculate the colour of every pixel in the Julia set. The full coding can be seen here, but the essential logic is as follows:

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
  }
  • As you can see, the functions returned by calling makeJuliaXStepFn and makeJuliaYStepFn outside the for loop are passed to juliaIter which then does all the hard work of calculating the colour of the current pixel

When I looked at this structure of code, at first I thought "This fine, it all works nicely; so nothing wrong here"

Except there was. The performance was much slower than expected...

Unexpected Solution

Much head scratching and fiddling around followed...

After a while, I discovered that if I move the creation of functions juliaXStepFn and juliaYStepFn inside either the outer or inner for loops, then the performance improves by a factor of between 2 and 3...

WHAAAAAAT!?

So, the code now looks like this

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 / (c

<details>
<summary>英文:</summary>

## Update (Mar 2nd, 2020)

It turns out that the coding in my example here was structured in just the right way to fall off a known performance cliff in the V8 JavaScript engine...

See the discussion over on [bugs.chromium.org](https://bugs.chromium.org/p/v8/issues/detail?id=10100) for the details.  This bug is now being worked on and should be fixed in the near future.

## Update (Jan 9th, 2020)

I tried to isolate the coding that behaves in the manner described below into a single page Web app, but in doing so, the behaviour disappeared(??).  However, the behaviour described below does still exist in the context of the full application.

That said, I have since optimised the fractal calculation coding and this problem is no longer an issue in the live version.  Should anyone be interested, the JavaScript module that manifests this problem is still available [here](http://whealy.com/Rust/js/fractal_old.js)


# Overview

I&#39;ve just completed a small Web-based app to compare the performance of browser-based JavaScript with Web Assembly.  This app calculates a Mandelbrot Set image, then as you move the mouse pointer over that image, the corresponding Julia Set is dynamically calculated and the calculation time is displayed.

You can switch between using JavaScript (press &#39;j&#39;) or WebAssembly (press &#39;w&#39;) to perform the calculation and compare runtimes.

Click [here](http://whealy.com/Rust/mandelbrot.html) to see the working app

However, in writing this code, I discovered some unexpectedly strange JavaScript performance behaviour...

# Problem Summary

1. This problem seems to be specific to the V8 JavaScript engine used in Chrome and Brave.  This problem does not appear in browsers using SpiderMonkey (Firefox) or JavaScriptCore (Safari).  I have not been able to test this in a browser using the Chakra engine

1. All the JavaScript code for this Web app has been written as [ES6 Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)

3. I&#39;ve tried rewriting all the functions using the traditional `function` syntax rather than the new ES6 arrow syntax.  Unfortunately, this does not make any appreciable difference

The performance problem seems to relate to the scope within which a JavaScript function is created.  In this app, I call two partial functions, each of which gives me back another function.  I then pass these generated functions as arguments to another function that is called inside a nested `for` loop.

Relative to the function within which it executes, it appears that a `for` loop creates something resembling its own scope (not sure its a full-blown scope though).  Then, passing generated functions across this scope(?) boundary is expensive.

# Basic Coding Structure

Each partial function receives the X or Y value of the mouse pointer&#39;s position over the Mandelbrot Set image, and returns the function to be iterated when calculating the corresponding Julia set:

```JavaScript
const makeJuliaXStepFn = mandelXCoord =&gt; (x, y) =&gt; mandelXCoord + diffOfSquares(x, y)
const makeJuliaYStepFn = mandelYCoord =&gt; (x, y) =&gt; mandelYCoord + (2 * x * y)

These functions are called within the following logic:

  • The user moves the mouse pointer over the image of the Mandelbrot Set triggering the mousemove event

  • The current location of the mouse pointer is translated to the coordinate space of Mandelbrot set and the (X,Y) coordinates are passed to function juliaCalcJS to calculate the corresponding Julia Set.

  • When creating any particular Julia Set, the above two partial functions are called to generate the functions to be iterated when creating the Julia Set

  • A nested for loop then calls function juliaIter to calculate the colour of every pixel in the Julia set. The full coding can be seen here, but the essential logic is as follows:

    const juliaCalcJS =
      (cvs, juliaSpace) =&gt; {
        // 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 &lt; cvs.height; ++iy) {
          for (let ix = 0; ix &lt; 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
      }
    
  • As you can see, the functions returned by calling makeJuliaXStepFn and makeJuliaYStepFn outside the for loop are passed to juliaIter which then does all the hard work of calculating the colour of the current pixel

When I looked at this structure of code, at first I thought "This fine, it all works nicely; so nothing wrong here"

Except there was. The performance was much slower than expected...

Unexpected Solution

Much head scratching and fiddling around followed...

After a while, I discovered that if I move the creation of functions juliaXStepFn and juliaYStepFn inside either the outer or inner for loops, then the performance improves by a factor of between 2 and 3...

WHAAAAAAT!?

So, the code now looks like this

const juliaCalcJS =
  (cvs, juliaSpace) =&gt; {
    // Snip - initialise canvas and create a new image array
    
    // For each pixel in the canvas...
    for (let iy = 0; iy &lt; 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 &lt; 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
  }

I would have expected this seemingly insignificant change to be somewhat less efficient, because a pair of functions that do not need to change are being recreated each time we iterate the for loop. Yet, by moving the function declarations inside the for loop, this code executes between 2 and 3 times faster!

Can anyone explain this behaviour?

Thanks

答案1

得分: 1

我的代码在V8 JavaScript引擎中不慎掉入了已知的性能陷阱...

有关问题和修复的详细信息请参阅bugs.chromium.org

英文:

My code managed to fall off a known performance cliff in the V8 JavaScript engine...

The details of the problem and the fix are described on bugs.chromium.org

huangapple
  • 本文由 发表于 2020年1月7日 01:19:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/59616335.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定