Тонкощі Javascript/Node.js. Збільшуємо продуктивність в десятки разів

Вступ

З'явилася необхідність обмінюватися повідомленнями між сервером і клієнтом в бінарному вигляді, але у форматі JSON в кінцевому підсумку. Почав я гуглити, які існують бібліотеки упаковки в бінарний вигляд. Переглянув чимало: MesssagePack, Bson, protobuf, capnproto.org та інші. Але всі ці бібліотеки дозволяють пакувати і розпаковувати готові бінарні пакети. Не дуже порпався, можливо робити парсер вхідного трафіку по шматках. Але суть не в цьому. З таким завданням ніколи не стикався і вирішив погратися з нодой і зробити свій. Куди ж без милиць і велосипедів? І ось з якими особливостями Node.js я зіткнувся…

Написав я пакер і запустив…

var start = Date.now();

for (i=0; i < 1000000; i++) {
packer.pack({abc: 123, cde: 5});
}

console.log(Date.now() - start);

Видав ~4300. Здивувався… Чому так довго? В той час, як код:

var start = Date.now();

for (i=0; i < 1000000; i++) {
JSON.stringify({abc: 123, cde: 5});
}

console.log(Date.now() - start);

Видав ~350. Не зрозумів. Почав копати свій код і шукати, де ж багато ресурсів використовується. І знайшов.

Запустимо цей код:

function find(val){

function index (value) {
return [1,2,3].indexOf(value);
}

return index(val);
}

var start = Date.now();

for (i=0; i < 1000000; i++) {
find(2);
}

console.log(Date.now() - start);

Видає 1908. Ви скажете: так це не багато на 1000000 повторень. А якщо я скажу, що багато? Виконаємо такий код:

function index (value) {
return [1,2,3].indexOf(value);
}

function find(val){

return index(val);

}

var start = Date.now();

for (i=0; i < 1000000; i++) {
find(2);
}

console.log(Date.now() - start);

Видає 16. Мої колеги теж обурилися, але і помітили, що функція ж створюється динамічно і відразу знищується, ти її виніс і немає такого навантаження. З експерименту висновок: динамічні фунції не кешируюся в бінарному вигляді. Я погодився і заперечив: так, але немає ні змінних у SCOPE нічого використовується всередині неї. Схоже, движок гугла завжди копіює SCOPE.

Ок. Провів оптимізацію цієї фунциональности і запустив… і все одно. Видав ~3000. Знову здивувався. І знову поліз копати… і виявив вже інший прикол.

Запустимо цей код:

function test (object) {

var a = 1,
b = [],
c = 0

return {
abc: function (val) {

}
}
}

var start = Date.now();

for (i=0; i < 1000000; i++) {
var a = test();
a.abc();
}

console.log(Date.now() - start);

Видав 34. Тепер, припустимо, нам треба всередині abc Array:

function test (object) {

var a = 1,
b = [],
c = 0

return {
abc: function () {
var arr1 = [];
}
}
}

var start = Date.now();

for (i=0; i < 1000000; i++) {
var a = test();
a.abc();
}

console.log(Date.now() - start);

Видав 1826. Сутеніло… А якщо нам треба 3 масиву?

function test (object) {

var a = 1,
b = [],
c = 0

return {
abc: function () {
var arr1 = [], arr2 = [], arr3 = [];
}
}
}

var start = Date.now();

for (i=0; i < 1000000; i++) {
var a = test();
a.abc();
}

console.log(Date.now() - start);

Видав 5302! Ось це приколи. Здавалося, SCOPE ми не використовуємо, а створення порожнього масиву повинне займати взагалі копійки. Не тут то було.

Думаю… А заміню-ка я на об'єкти. Результат трохи краще, але не набагато. Видав 1071.

А тепер фокус. Багато хто скаже: ти ж знову виносиш функцію. Так. Але фокус в іншому.


function abc () {
var arr1 = [], arr2 = [], arr3 = [];
}

function test (object) {

var a = 1,
b = [],
c = 0

return {
abc: abc
}
}

var start = Date.now();

for (i=0; i < 1000000; i++) {
var a = test();
a.abc();
}

console.log(Date.now() - start);

Багато помітять і скажуть: буде такий же час. А не тут то було. Видав 25. Хоча масиви створювалися стільки ж разів. Робимо висновок: створення масивів в динамічної функції витрачає багато ресурсів. Питання: чому?

Тепер повернемося до першої проблеми. Але з іншого боку. Винесемо Array:

var indexes = [1,2,3];

function find(val){

function index (value) {
return indexes.indexOf(value);
}

return index(val);
}

var start = Date.now();

for (i=0; i < 1000000; i++) {
find(2);
}

console.log(Date.now() - start);

І я був правий. Видав 58. З винесенням всій фунції видавав 16. Тобто створення функції не особливо ресурсомісткий процес. Також спростовуємо минулий висновок:
бінарний код функції кешується в пам'яті. А створення об'єктів динамічної функції займає багато часу.
Я раніше припускав по-іншому: все static/expression об'єкти, створювані тимчасово, компілюються відразу як код функції. А, виявляється, немає. Робимо висновок:
движок гугла при кожному запуску створює нові об'єкти і заповнює необхідними значеннями, а потім вже обчислює вираз, що не добре.
А з якими тонкощами стикалися ви? Коментарі вітаються.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.