【发布时间】:2020-01-06 17:19:21
【问题描述】:
更新(2020 年 3 月 2 日)
事实证明,我的示例中的编码以正确的方式构建,以摆脱 V8 JavaScript 引擎中已知的性能悬崖...
有关详细信息,请参阅bugs.chromium.org 上的讨论。这个错误现在正在处理中,应该会在不久的将来修复。
更新(2020 年 1 月 9 日)
我尝试将具有下述行为方式的代码隔离到一个单页 Web 应用程序中,但这样做时,行为消失了 (??)。但是,下面描述的行为仍然存在于完整应用程序的上下文中。
也就是说,我已经优化了分形计算编码,这个问题在正式版中不再是问题。如果有人有兴趣,可以使用显示此问题的 JavaScript 模块here
概述
我刚刚完成了一个基于 Web 的小型应用程序,用于比较基于浏览器的 JavaScript 与 Web Assembly 的性能。此应用计算 Mandelbrot 集图像,然后当您将鼠标指针移到该图像上时,会动态计算相应的 Julia 集并显示计算时间。
您可以在使用 JavaScript(按“j”)或 WebAssembly(按“w”)之间切换来执行计算并比较运行时。
点击here查看正在运行的应用程序
然而,在编写这段代码时,我发现了一些意想不到的奇怪的 JavaScript 性能行为......
问题总结
这个问题似乎是 Chrome 和 Brave 中使用的 V8 JavaScript 引擎特有的。此问题不会出现在使用 SpiderMonkey (Firefox) 或 JavaScriptCore (Safari) 的浏览器中。我无法在使用 Chakra 引擎的浏览器中对此进行测试
此 Web 应用程序的所有 JavaScript 代码均已编写为 ES6 Modules
我尝试使用传统的
function语法而不是新的 ES6 箭头语法重写所有函数。不幸的是,这并没有任何明显的区别
性能问题似乎与创建 JavaScript 函数的范围有关。在这个应用程序中,我调用了两个部分函数,每个函数都返回另一个函数。然后我将这些生成的函数作为参数传递给在嵌套的for 循环中调用的另一个函数。
相对于它执行的函数,for 循环似乎创建了类似于它自己的作用域的东西(虽然不确定它是一个成熟的作用域)。然后,跨这个范围(?)边界传递生成的函数是昂贵的。
基本编码结构
每个偏函数接收鼠标指针在Mandelbrot Set图像上位置的X或Y值,并在计算对应的Julia集时返回要迭代的函数:
const makeJuliaXStepFn = mandelXCoord => (x, y) => mandelXCoord + diffOfSquares(x, y)
const makeJuliaYStepFn = mandelYCoord => (x, y) => mandelYCoord + (2 * x * y)
这些函数在以下逻辑中被调用:
- 用户将鼠标指针移动到触发
mousemove事件的 Mandelbrot 集的图像上 -
鼠标指针的当前位置被翻译到Mandelbrot集合的坐标空间,并将(X,Y)坐标传递给函数
李>juliaCalcJS计算对应的Julia Set。 在创建任何特定的 Julia Set 时,会调用上述两个偏函数来生成创建 Julia Set 时要迭代的函数
-
嵌套的
for循环然后调用函数juliaIter来计算Julia 集中每个像素的颜色。完整编码可见here,但基本逻辑如下: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 } 如您所见,在
for循环外调用makeJuliaXStepFn和makeJuliaYStepFn返回的函数将传递给juliaIter,然后由它完成计算当前像素颜色的所有艰苦工作
当我看到这个代码结构时,起初我认为“这很好,一切都很好;所以这里没有错”
除了有。性能比预期的要慢很多...
意外的解决方案
随之而来的是许多挠头和摆弄......
过了一会儿,我发现如果我将函数 juliaXStepFn 和 juliaYStepFn 的创建移动到外部或内部 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 倍!
谁能解释这种行为?
谢谢
【问题讨论】:
-
难住了,但我确实很喜欢查看您的网站和那里的应用程序。真是好东西!
-
感谢@Calculuswhiz,这似乎是 Chrome/Brave 特有的问题。 Safari 和 Firefox 似乎没有受到影响。我会相应地更新帖子
-
这是一个非常详细的总结......你有什么理由在一般编程问答网站上提交基本上是 V8 票证,而不是在the V8 issue tracker 上?
-
他确实在 V8 跟踪器上发布了一个问题。这是那里的第一个
-
我的猜测是,将所有内容都包含在迭代中可以简化优化器的依赖关系图,从而能够生成更好的代码。 v8 profiler 可能会更清楚地了解正在发生的事情。
标签: javascript performance scope