[Из песочницы] Тонкости 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 объекты, создаваемые временно, компилируются сразу как код функции. А, оказывается, нет. Делаем вывод:

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


А с какими тонкостями сталкивались вы? Комментарии приветствуются.

© Habrahabr.ru