Перевод статьи Benedikt Meurer: JavaScript Optimization Patterns (Part 1).
Прошло время с моего последнего сообщения в блоге, главным образом из-за того, что мне не хватало времени или сил, чтобы сесть и написать все, что я хотел рассказать. Частично это было потому, что я был очень занят запуском Ignition
и TurboFan
в Chrome 59
, что, к счастью, завершилось успехом. Частично из-за того, что я хотел провести время с семьей. И последнее, но не менее важное, я участвовал в JSConf EU и Web Rebels, и на момент написания этой статьи я нахожусь на enterJS, прокрастинируя вместо отшлифовки последних правок в моём докладе.
Тем временем я только что вернулся с очень интересного обеденного обсуждения с Брайаном Терлсоном, Адой Роуз Эдвардс и Эшли Уильямс о хороших подходах к оптимизации JavaScript, которые мы можем дать в качестве совета, и в частности, о том, как трудно их вывести. Один конкретный вывод, который я сделал, заключался в том, что идеальная производительность часто зависит от контекста, в котором работает код, и это часто самая сложная часть. Поэтому я подумал, что, вероятно, стоит поделиться этой информацией со всеми. Я начну это как серию сообщений в блоге. В этой первой части я попытаюсь выделить влияние, которое может оказать конкретный контекст выполнения на производительность вашего JavaScript-кода.
Рассмотрим следующий искусственный класс Point
, имеющий метод distance
, который вычисляет «манхэттенское расстояние» между двумя точками.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distance(other) {
const dx = Math.abs(this.x - other.x);
const dy = Math.abs(this.y - other.y);
return dx + dy;
}
}
В дополнение к этому рассмотрим следующую тестирующую функцию, создающую пару экземпляров Point
и вычисляющую расстояние между ними несколько миллионов раз, суммируя результат (да, я знаю, что это микро-бенчмарк, но потерпите немножко):
function test() {
const points = [
new Point(10, 10),
new Point(1, 1),
new Point(8, 9)
];
let result = 0;
for (let i = 0; i < 10000000; ++i) {
for (const point1 of points) {
for (const point2 of points) {
result += point1.distance(point2);
}
}
}
return result;
}
Теперь у нас есть правильный бенчмарк для класса Point
и, в частности, его метод distance
. Давайте проведём несколько запусков тестирующей функции, чтобы узнать, что такое производительность, используя следующий HTML-сниппет:
<script>
function test() {
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distance(other) {
const dx = Math.abs(this.x - other.x);
const dy = Math.abs(this.y - other.y);
return dx + dy;
}
}
const points = [
new Point(10, 10),
new Point(1, 1),
new Point(8, 9)
];
let result = 0;
for (let i = 0; i < 10000000; ++i) {
for (const point1 of points) {
for (const point2 of points) {
result += point1.distance(point2);
}
}
}
return result;
}
for (let i = 1; i <= 5; ++i) {
console.time("test " + i);
test();
console.timeEnd("test " + i);
}
</script>
Если вы запустите это в Chrome 61 (Canary), в консоли Chrome Developer Tools вы увидите следующий вывод:
test 1: 595.248046875ms
test 2: 765.451904296875ms
test 3: 930.452880859375ms
test 4: 994.2890625ms
test 5: 3894.27392578125ms
Производительность отдельных прогонов очень противоречива. Вы можете видеть, что производительность становится хуже с каждым последующим запуском. Причина регрессии производительности заключается в том, что класс Point
находится внутри тестирующей функции.
<script>
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distance(other) {
const dx = Math.abs(this.x - other.x);
const dy = Math.abs(this.y - other.y);
return dx + dy;
}
}
function test() {
const points = [
new Point(10, 10),
new Point(1, 1),
new Point(8, 9)
];
let result = 0;
for (let i = 0; i < 10000000; ++i) {
for (const point1 of points) {
for (const point2 of points) {
result += point1.distance(point2);
}
}
}
return result;
}
for (let i = 1; i <= 5; ++i) {
console.time("test " + i);
test();
console.timeEnd("test " + i);
}
</script>
Если мы немного изменим фрагмент кода таким образом, чтобы класс Point
был определен вне тестовой функции, мы получим другие результаты:
test 1: 598.794921875ms
test 2: 599.18115234375ms
test 3: 600.410888671875ms
test 4: 608.98388671875ms
test 5: 605.36376953125ms
Теперь производительность довольно стабильна с небольшим шумом. Обратите внимание, что в обоих случаях мы использовали точно такой же код для класса Point
и точно такой же код для логики тестового драйвера. Единственное различие заключается в том, где именно мы размещаем класс Point
в коде.
Также стоит отметить, что это не связанно с новым синтаксисом class
ES2015: при использовании старого ES5-синтаксиса для класса Point
мы получим такие же результаты.
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.distance = function (other) {
var dx = Math.abs(this.x - other.x);
var dy = Math.abs(this.y - other.y);
return dx + dy;
}
Основная причина разницы в производительности, когда объявление класса Point
расположено внутри функции test
, заключается в том, что литерал class
выполняется несколько раз (ровно 5 раз в моем примере выше), тогда как если он расположен вне функции test
, он выполняется только один раз. Каждый раз, когда выполняется определение класса, создаётся новый объект-прототип, содержащий все методы класса. В дополнение к этому создаётся новый конструктор, соответствующий классу и имеющий объект прототипа, заданный как свойство prototype
.
Новые экземпляры класса создаются с использованием этого свойства prototype
в качестве объекта прототипа. Но так как V8 отслеживает прототип экземпляра как часть формы объекта или скрытого класса (см. раздел «Setting up prototypes in V8»), чтобы оптимизировать доступ к свойствам в цепочке прототипов, наличие разных прототипов автоматически подразумевает наличие разных форм у этих объектов. И как таковой сгенерированный код становится все более полиморфным с каждым новым определение класса, и, в конце концов, V8 отказывается от полиморфизма после того, как он видит более четырёх различных форм объектов и входит в так называемое мегаморфное состояние, что означает отказ от генерации высоко оптимизированного кода.
Таким образом, вывод из этого упражнения: идентичный код, помещенный в другое место, может легко привести к разнице в производительности в 6,5 раз! Это особенно важно, потому что популярные платформы для бенчмарков и сайты, такие как esbench.com, как правило, выполняют код в другом контексте, чем ваше приложение (например, код вспомогательных функций-обёрток, запускающихся несколько раз), и, таким образом, результаты бенчмарков могут ввести в сильное заблуждение.
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.