Кто-нибудь может объяснить это неожиданное поведение производительности V8 JavaScript? - PullRequest
8 голосов
/ 06 января 2020

Обновление (2 марта 2020 г.)

Оказывается, что кодирование в моем примере здесь было структурировано совершенно правильно, чтобы упасть с обрыва известной производительности в движке V8 JavaScript ...

Подробнее см. Обсуждение на bugs.chromium.org . Эта ошибка в настоящее время работает и должна быть исправлена ​​в ближайшем будущем.

Обновление (9 января 2020 г.)

Я попытался выделить кодирование, которое ведет себя так, как описано ниже, в одностраничное веб-приложение, но при этом поведение исчезло (??). Однако описанное ниже поведение все еще существует в контексте полного приложения.

Тем не менее, с тех пор я оптимизировал кодирование вычисления фрактала, и эта проблема больше не является проблемой в живой версии. Если кому-то будет интересно, модуль JavaScript, который обнаруживает эту проблему, все еще доступен здесь

Обзор

Я только что закончил небольшое веб-приложение для сравнения производительность на основе браузера JavaScript с веб-сборкой. Это приложение вычисляет изображение набора Мандельброта, а затем, когда вы наводите указатель мыши на это изображение, соответствующий набор Джулии вычисляется динамически, и отображается время расчета.

Вы можете переключаться между JavaScript (нажмите ' j ') или WebAssembly (нажмите' w '), чтобы выполнить расчет и сравнить время выполнения.

Нажмите здесь , чтобы увидеть работающее приложение

Однако при написании этого кода , Я обнаружил неожиданно странное поведение производительности JavaScript ...

Краткое описание проблемы

  1. Эта проблема, похоже, относится c к движку V8 JavaScript используется в Chrome и Храбрый. Эта проблема не появляется в браузерах, использующих SpiderMonkey (Firefox) или JavaScriptCore (Safari). Я не смог проверить это в браузере с использованием механизма Chakra

  2. Весь код JavaScript для этого веб-приложения был написан как ES6 Modules

  3. Я пытался переписать все функции, используя традиционный синтаксис 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 раза быстрее!

Кто-нибудь может объяснить это поведение?

Спасибо

1 Ответ

1 голос
/ 02 марта 2020

Моему коду удалось упасть с известной производительности в движке V8 JavaScript ...

Подробности проблемы и ее исправления описаны на bugs.chromium.org

...