Обновление (2 марта 2020 г.)
Оказывается, что кодирование в моем примере здесь было структурировано совершенно правильно, чтобы упасть с обрыва известной производительности в движке V8 JavaScript ...
Подробнее см. Обсуждение на bugs.chromium.org . Эта ошибка в настоящее время работает и должна быть исправлена в ближайшем будущем.
Обновление (9 января 2020 г.)
Я попытался выделить кодирование, которое ведет себя так, как описано ниже, в одностраничное веб-приложение, но при этом поведение исчезло (??). Однако описанное ниже поведение все еще существует в контексте полного приложения.
Тем не менее, с тех пор я оптимизировал кодирование вычисления фрактала, и эта проблема больше не является проблемой в живой версии. Если кому-то будет интересно, модуль JavaScript, который обнаруживает эту проблему, все еще доступен здесь
Обзор
Я только что закончил небольшое веб-приложение для сравнения производительность на основе браузера JavaScript с веб-сборкой. Это приложение вычисляет изображение набора Мандельброта, а затем, когда вы наводите указатель мыши на это изображение, соответствующий набор Джулии вычисляется динамически, и отображается время расчета.
Вы можете переключаться между JavaScript (нажмите ' j ') или WebAssembly (нажмите' w '), чтобы выполнить расчет и сравнить время выполнения.
Нажмите здесь , чтобы увидеть работающее приложение
Однако при написании этого кода , Я обнаружил неожиданно странное поведение производительности JavaScript ...
Краткое описание проблемы
Эта проблема, похоже, относится c к движку V8 JavaScript используется в Chrome и Храбрый. Эта проблема не появляется в браузерах, использующих SpiderMonkey (Firefox) или JavaScriptCore (Safari). Я не смог проверить это в браузере с использованием механизма Chakra
Весь код JavaScript для этого веб-приложения был написан как ES6 Modules
Я пытался переписать все функции, используя традиционный синтаксис function
, а не новый синтаксис стрелки ES6. К сожалению, это не имеет существенного значения
Кажется, что проблема с производительностью связана с областью, в которой создается функция JavaScript. В этом приложении я вызываю две частичные функции, каждая из которых возвращает мне другую функцию. Затем я передаю эти сгенерированные функции в качестве аргументов другой функции, которая вызывается внутри вложенного for
l oop.
. Относительно функции, внутри которой она выполняется, создается впечатление, что for
l oop создает что-то похожее на свою собственную область (хотя не уверен, что это полная область). Тогда передача сгенерированных функций через эту область видимости (?) Обходится дорого.
Basi c Структура кодирования
Каждая частичная функция получает значение X или Y позиции указателя мыши над Мандельбротом Установите изображение и возвращает функцию, которая будет повторяться при расчете соответствующего набора Джулии:
const makeJuliaXStepFn = mandelXCoord => (x, y) => mandelXCoord + diffOfSquares(x, y)
const makeJuliaYStepFn = mandelYCoord => (x, y) => mandelYCoord + (2 * x * y)
Эти функции вызываются в следующих логах c:
- Пользователь перемещает указатель мыши над изображением набора Мандельброта, запускающий событие
mousemove
Текущее положение указателя мыши переводится в пространство координат набора Мандельброта, а координаты (X, Y) передается функции juliaCalcJS
для вычисления соответствующего набора Джулии.
При создании любого конкретного набора Джулии две вышеупомянутые частичные функции вызываются для генерации функций, которые будут повторяться при создании Джулии. Установите
Вложенный for
l oop, затем вызывает функцию juliaIter
для вычисления цвет каждого пикселя в наборе Julia. Полное кодирование можно увидеть здесь , но основные логики c таковы:
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
}
Как видите, функции, возвращаемые вызовом makeJuliaXStepFn
и makeJuliaYStepFn
вне for
l oop, передаются в juliaIter
, который затем выполняет всю тяжелую работу по вычислению цвета текущий пиксель
Когда я посмотрел на эту структуру кода, я сначала подумал «Это хорошо, все прекрасно работает, поэтому здесь все в порядке»
За исключением того, что было. Производительность была намного медленнее, чем ожидалось ...
Неожиданное решение
Последовало сильное царапание и тряска головы ...
Через некоторое время я обнаружил, что если я перееду создание функций juliaXStepFn
и juliaYStepFn
во внешнем или внутреннем цикле for
, затем производительность улучшается в 2–3 раза ...
WHAAAAAAT!?
Итак, код теперь выглядит следующим образом
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
l oop. Тем не менее, перемещая объявления функций внутри for
l oop, этот код выполняется в 2–3 раза быстрее!
Кто-нибудь может объяснить это поведение?
Спасибо