谁能解释这种意想不到的 V8 JavaScript 性能行为



更新(2020年3月2日)

事实证明,我这里的例子中的编码结构恰到好处,可以在 V8 JavaScript 引擎中从已知的性能悬崖上掉下来......

有关详细信息,请参阅 bugs.chromium.org 上的讨论。 此错误目前正在处理中,应在不久的将来修复。

更新(2020年1月9日)

我试图将以下描述的方式行为的编码隔离到一个页面 Web 应用程序中,但这样做时,行为消失了(?? 但是,下面描述的行为在完整应用程序的上下文中仍然存在。

也就是说,我已经优化了分形计算编码,这个问题在实时版本中不再是问题。 如果有人感兴趣,出现此问题的 JavaScript 模块仍然在这里可用

概述

我刚刚完成了一个基于 Web 的小型应用程序,用于比较基于浏览器的 JavaScript 与 Web Assembly 的性能。 此应用程序计算曼德布洛特集图像,然后当您将鼠标指针移到该图像上时,将动态计算相应的朱莉娅集并显示计算时间。

您可以在使用 JavaScript(按"j")或 WebAssembly(按"w")之间切换来执行计算和比较运行时。

单击此处查看工作应用程序

然而,在编写这段代码时,我发现了一些意想不到的奇怪的 JavaScript 性能行为......

问题摘要

  1. 这个问题似乎是Chrome和Brave中使用的V8 JavaScript引擎特有的。 此问题不会出现在使用SpiderMonkey(Firefox)或JavaScriptCore(Safari)的浏览器中。 我无法使用Chakra引擎在浏览器中对此进行测试

  2. 此 Web 应用程序的所有 JavaScript 代码都已编写为 ES6 模块

  3. 我尝试使用传统的function语法而不是新的 ES6 箭头语法重写所有函数。 不幸的是,这并没有产生任何明显的区别

性能问题似乎与创建 JavaScript 函数的范围有关。 在这个应用程序中,我调用两个部分函数,每个函数都给我另一个函数。 然后,我将这些生成的函数作为参数传递给在嵌套for循环中调用的另一个函数。

相对于它执行的函数,for循环似乎创建了类似于其自身作用域的东西(但不确定它是一个成熟的作用域)。 然后,跨此范围(?)边界传递生成的函数是昂贵的。

基本编码结构

每个偏函数接收鼠标指针在曼德布洛特集图像上位置的 X 或 Y 值,并在计算相应的 Julia 集时返回要迭代的函数:

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

这些函数在以下逻辑中调用:

  • 用户将鼠标指针移到触发mousemove事件的曼德布洛特集的图像上
  • 鼠标指针的当前位置被转换为曼德布洛特集合的坐标空间,并将 (X,Y) 坐标传递给函数juliaCalcJS以计算相应的 Julia Set。

  • 创建任何特定的 Julia Set 时,将调用上述两个部分函数以生成创建 Julia Set 时要迭代的函数

  • 然后,嵌套for循环调用函数juliaIter来计算 Julia 集中每个像素的颜色。 完整的编码可以在这里看到,但基本逻辑如下:

    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
    }
    
  • 如您所见,通过调用makeJuliaXStepFnmakeJuliaYStepFnfor循环之外返回的函数被传递给juliaIter然后由完成计算当前像素颜色的所有艰苦工作

当我看到这种代码结构时,起初我想"这很好,一切都很好;所以这里没有错">

除了有。 性能比预期的要慢得多...

意想不到的解决方案

随之而来的是许多挠头和摆弄...

一段时间后,我发现如果我将函数的创建juliaXStepFnjuliaYStepFn在外部或内部for循环中移动,那么性能会提高 2 到 3 倍......

咦!?

所以,代码现在看起来像这样

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
}

我本以为这个看似微不足道的更改效率会降低一些,因为每次我们迭代for循环时,都会重新创建一对不需要更改的函数。 然而,通过在for循环中移动函数声明,此代码的执行速度提高了 2 到 3 倍!

谁能解释这种行为?

谢谢

我的代码设法在 V8 JavaScript 引擎中从已知的性能悬崖上掉下来......

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

最新更新