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