Статьи

Снижение давления на сборщик мусора с помощью панели разработчика F12 для IE11

Как вы, наверное, знаете, я работаю над 3D движком для WebGL ( Babylon.js ) в свободное время. 3D-движок — это место, где живут матрицы, векторы и кватернионы. И их может быть много!

Обратите внимание, что все, что сделано здесь, относится к приложениям Internet Explorer 11 и Windows 8.1, разработанным с использованием HTML5 / JavaScript.

Удаление ненужных экземпляров

Например, давайте посмотрим на эту сцену:

образ

Используя панель разработчика F12, вы можете запустить профилировщик для анализа происходящего с точки зрения производительности. В профилировщике есть кнопки «Пуск» и «Стоп», чтобы зафиксировать период времени, а затем отобразить этот экран:

образ

Функция drawElements — это функция, которая * EFFECTIVELY * визуализирует объекты. Тогда мы не удивимся, когда получим это первыми. Второй ( умножение ) — это умножение двух матриц. Эта функция используется много. Действительно, для каждого объекта вы должны вычислить матрицу, необходимую для его рисования, матрицу для вычисления положения каждой текстуры и т. Д.

умножение используется более 12 000 раз в течение двух секунд!

Вот код для этой функции:

BABYLON.Matrix.prototype.multiply = function (other) {
    var result = new BABYLON.Matrix();

    result.m[0] = this.m[0] * other.m[0] + this.m[1] * other.m[4] + this.m[2] * other.m[8] + this.m[3] * other.m[12];
    result.m[1] = this.m[0] * other.m[1] + this.m[1] * other.m[5] + this.m[2] * other.m[9] + this.m[3] * other.m[13];
    result.m[2] = this.m[0] * other.m[2] + this.m[1] * other.m[6] + this.m[2] * other.m[10] + this.m[3] * other.m[14];
    result.m[3] = this.m[0] * other.m[3] + this.m[1] * other.m[7] + this.m[2] * other.m[11] + this.m[3] * other.m[15];

    result.m[4] = this.m[4] * other.m[0] + this.m[5] * other.m[4] + this.m[6] * other.m[8] + this.m[7] * other.m[12];
    result.m[5] = this.m[4] * other.m[1] + this.m[5] * other.m[5] + this.m[6] * other.m[9] + this.m[7] * other.m[13];
    result.m[6] = this.m[4] * other.m[2] + this.m[5] * other.m[6] + this.m[6] * other.m[10] + this.m[7] * other.m[14];
    result.m[7] = this.m[4] * other.m[3] + this.m[5] * other.m[7] + this.m[6] * other.m[11] + this.m[7] * other.m[15];

    result.m[8] = this.m[8] * other.m[0] + this.m[9] * other.m[4] + this.m[10] * other.m[8] + this.m[11] * other.m[12];
    result.m[9] = this.m[8] * other.m[1] + this.m[9] * other.m[5] + this.m[10] * other.m[9] + this.m[11] * other.m[13];
    result.m[10] = this.m[8] * other.m[2] + this.m[9] * other.m[6] + this.m[10] * other.m[10] + this.m[11] * other.m[14];
    result.m[11] = this.m[8] * other.m[3] + this.m[9] * other.m[7] + this.m[10] * other.m[11] + this.m[11] * other.m[15];

    result.m[12] = this.m[12] * other.m[0] + this.m[13] * other.m[4] + this.m[14] * other.m[8] + this.m[15] * other.m[12];
    result.m[13] = this.m[12] * other.m[1] + this.m[13] * other.m[5] + this.m[14] * other.m[9] + this.m[15] * other.m[13];
    result.m[14] = this.m[12] * other.m[2] + this.m[13] * other.m[6] + this.m[14] * other.m[10] + this.m[15] * other.m[14];
    result.m[15] = this.m[12] * other.m[3] + this.m[13] * other.m[7] + this.m[14] * other.m[11] + this.m[15] * other.m[15];

    return result;
};

Это немного жестоко, но в этом нет ничего сложного.

Ситуация сходит с ума, когда вы используете панель разработчика F12 для отслеживания реакции вашей страницы

образ

Как видите, сборщик мусора (оранжевые столбики) вызывается очень часто! И это не очень хорошая вещь, потому что это может привести к визуальным сбоям из-за прерывания рендеринга ваших кадров.

На том же экране вы также можете получить более подробную информацию:

образ

Этот снимок показывает важную вещь: сборщик мусора работает в фоновом потоке (12516), что действительно хорошо для освобождения времени для потока рендеринга (вы можете видеть, что сборщик мусора запускается одновременно с обратным вызовом фрейма анимации, поскольку babylon.js использует requestAnimationFrame рендерить каждый кадр).

Даже если сборщик мусора в IE11 работает в фоновом потоке, мы должны уменьшить нагрузку на память. Это связано с тем, что наш код может работать на слабом оборудовании, где потоки недоступны, или потому, что не во всех браузерах есть фоновый сборщик мусора.

Поэтому, насколько вы можете, не полагайтесь на создание экземпляров ( новый BABYLON.Matrix () здесь). Вы должны предпочесть повторное использование объектов вместо создания новых. Обновленная функция умножения может быть:

BABYLON.Matrix.prototype.multiplyToRef = function (other, result) {
    result[0] = this.m[0] * other.m[0] + this.m[1] * other.m[4] + this.m[2] * other.m[8] + this.m[3] * other.m[12];
    result[1] = this.m[0] * other.m[1] + this.m[1] * other.m[5] + this.m[2] * other.m[9] + this.m[3] * other.m[13];
    result[2] = this.m[0] * other.m[2] + this.m[1] * other.m[6] + this.m[2] * other.m[10] + this.m[3] * other.m[14];
    result[3] = this.m[0] * other.m[3] + this.m[1] * other.m[7] + this.m[2] * other.m[11] + this.m[3] * other.m[15];

    result[4] = this.m[4] * other.m[0] + this.m[5] * other.m[4] + this.m[6] * other.m[8] + this.m[7] * other.m[12];
    result[5] = this.m[4] * other.m[1] + this.m[5] * other.m[5] + this.m[6] * other.m[9] + this.m[7] * other.m[13];
    result[6] = this.m[4] * other.m[2] + this.m[5] * other.m[6] + this.m[6] * other.m[10] + this.m[7] * other.m[14];
    result[7] = this.m[4] * other.m[3] + this.m[5] * other.m[7] + this.m[6] * other.m[11] + this.m[7] * other.m[15];

    result[8] = this.m[8] * other.m[0] + this.m[9] * other.m[4] + this.m[10] * other.m[8] + this.m[11] * other.m[12];
    result[9] = this.m[8] * other.m[1] + this.m[9] * other.m[5] + this.m[10] * other.m[9] + this.m[11] * other.m[13];
    result[10] = this.m[8] * other.m[2] + this.m[9] * other.m[6] + this.m[10] * other.m[10] + this.m[11] * other.m[14];
    result[11] = this.m[8] * other.m[3] + this.m[9] * other.m[7] + this.m[10] * other.m[11] + this.m[11] * other.m[15];

    result[12] = this.m[12] * other.m[0] + this.m[13] * other.m[4] + this.m[14] * other.m[8] + this.m[15] * other.m[12];
    result[13] = this.m[12] * other.m[1] + this.m[13] * other.m[5] + this.m[14] * other.m[9] + this.m[15] * other.m[13];
    result[14] = this.m[12] * other.m[2] + this.m[13] * other.m[6] + this.m[14] * other.m[10] + this.m[15] * other.m[14];
    result[15] = this.m[12] * other.m[3] + this.m[13] * other.m[7] + this.m[14] * other.m[11] + this.m[15] * other.m[15];
};

Почти то же самое без экземпляров! Но удаление 6000 экземпляров в секунду может быть хорошей оптимизацией!

Дело в том, что вы должны создать матрицу хранения для каждой операции (матрица создается один раз внутри конструктора и используется повторно каждый раз, когда необходимо использовать операцию умножения).

После того же действия для каждой функции, работающей с матрицами, векторами, цветами и кватернионами, график отзывчивости babylon.js стал намного лучше:

образ

Отлично, не правда ли? Очевидно, сборщик мусора будет вызываться, но не так часто.

GC-friendly Array Object

Я также нашел другое решение, чтобы снять давление памяти. Действительно, во время рендеринга кадра мне приходится использовать множество массивов для определения активных объектов, активных шейдеров, активных частиц и т. Д.

В основном в начале каждого кадра я использовал этот код:

this._activeMeshes = [];

Но, очевидно, даже если код прост, он сильно влияет на память. Вот почему я решил создать новый тип массива, который сможет повторно использовать первоначально выделенное пространство:

// Garbage collector friendly array
BABYLON.Tools.GCFriendlytArray = function (capacity) {
    this.data = new Array(capacity);
    this.length = 0;
};

BABYLON.Tools.GCFriendlytArray.prototype.push = function (value) {
    if (this.length >= this.data.length) {
        this.data.length *= 2;
    }
    this.data[this.length++] = value;
};

BABYLON.Tools.GCFriendlytArray.prototype.reset = function () {
    this.length = 0;
};

BABYLON.Tools.GCFriendlytArray.prototype.indexOf = function (value) {
    var position = this.data.indexOf(value);

    if (position >= this.length) {
        return -1;
    }

    return position;
};

С этим небольшим фрагментом кода вы можете получить массив, который можно сбросить для повторного использования его памяти. Вы можете создать его с приблизительным размером и просто вызвать reset (), чтобы сбросить его.Sourire

Использование нашего нового массива похоже на использование стандартного массива, у вас есть свойство length и функция push. Разница лишь в том, когда вы хотите получить доступ к данным, потому что вы должны использовать myArray.data:

for (subIndex = 0; subIndex < activeMeshes.length; subIndex++) {
    activeMeshes.data[subIndex].render();
}

Некоторые дополнительные заметки

Обратите внимание, что описанные здесь оптимизации полезны в моем случае, потому что я искал производительность, и мне было наплевать на потребление памяти. Действительно, мне пришлось использовать много памяти для создания необходимых кэшированных объектов.

Например, дружественные GC массивы должны зарезервировать много памяти, которая не понадобится. Компромисс между памятью и производительностью следует воспринимать серьезно.

Идти дальше

Здесь вы найдете несколько отличных ссылок о панели разработчика F12 для IE, выпущенной во время сборки 2013: