Вступление
Появилась необходимость обмениваться сообщениями между сервером и клиентом в бинарном виде, но в формате 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 объекты, создаваемые временно, компилируются сразу как код функции. А, оказывается, нет. Делаем вывод:
движок гугла при каждом запуске создает новые объекты и заполняет необходимыми значениями, а потом уже вычисляет выражение, что не хорошо.
А с какими тонкостями сталкивались вы? Комментарии приветствуются.
Комментарии (36)
kroshanin
24.02.2016 14:34+1Хотел поковыряться, но у меня почему-то и ваш начальный код, и ваш конечный код выдают одинаковое время, равное 20 мс, вот ссылка:
http://jsbin.com/hevikonuhu/edit?html,console,output
Возможно, я что-то делаю не так?peacecoder85
24.02.2016 14:43+1Попробуйте запустить на Node.js. Не знаю почему, но в браузере значительной разницы не вижу тоже. У меня выдает 776 для начального варианта и 716 для конечного.
Frozik
24.02.2016 15:33+1А какая версия ноды используется? Может просто в браузере более свежая версия движка V8?
peacecoder85
24.02.2016 16:03+1Тестировал в 4.2.2 (LTS). Сотрудник проводил на 5.x и тоже было такое.
Frozik
24.02.2016 16:04+1Хм, действительно странно. Где первая оптимизация с 1908 до 16мс.
Frozik
24.02.2016 16:09+1Случайно отправил и не успел отредактировать.
Хм, действительно странно. Где первая оптимизация с 1908мс до 16мс. У меня на ноде 5.7.0x64 выдает (2573мс и 32мс), а в хроме 48.0.2564.116x64 (1491мс и 1252мс).
bohdan4ik
24.02.2016 16:44+1Это же просто замыкания.
Создание нового замыканя потребляет ресурсы (ЦП+ОЗУ) для захвата области видимости, должно быть очевидно.
Нужна максимальня производительность — избегайте замыканий в любом виде.peacecoder85
24.02.2016 16:48+1Так в замыкании не используются переменные, которые выше.
bohdan4ik
24.02.2016 16:49+4Оптимизатор тупой, не может предугадать, что испльзуется, а что — нет. Может у вас там eval-magic где-то спрятана?
upd: я без наездов. Сам бы рад, чтобы он захватывал только необходимый минимум из доступных в области видимости данных, но увы. :(
peacecoder85
24.02.2016 16:50Нашел оптимизацию этой особенности: везде в замыканиях используйте new Object() / new Array()
Aquahawk
24.02.2016 17:25+1проверил только что на 5.6.0 ноде это дало ускорение. Приём не в замыканиях в тайпскрипте(хотя функции объявлены в в скоупе конструктора так что замыкания.)
zxcabs
24.02.2016 18:46+1Не все так однозначно с new Array
https://gist.github.com/zxcabs/5d75c11f69445c4d9837
Shannon
24.02.2016 18:05+6Немного не в тему, для замеров времени можно упростить:
console.time('first test'); тестируемый код console.timeEnd('first test');
Makeomatic
24.02.2016 23:24+3- канонично в node.js использовать process.hrtime() для измерения относительных промежутков времени — функция значительно точнее.
const start = process.hrtime(); // do op const end = process.hrtime(start); console.info("Время исполнения (hr): %ds %dms", end[0], end[1]/1000000);
var a
внутри циклаfor
— постоянное переобъявление переменной, используйтеlet
если нужно ограничить scope, или объявите до цикла
- штудируем https://github.com/petkaantonov/bluebird/wiki/Optimization-killers касательно оптимизаций — многие вопросы отпадут сами собой
- канонично в node.js использовать process.hrtime() для измерения относительных промежутков времени — функция значительно точнее.
forgotten
24.02.2016 18:56+1О господи, очередные откровения «как нам ускорить JS-код».
Подымите руку, у кого в стандартной бизнес-логике (сходить в три бэкенда и сшаблонизировать данные) есть самописные циклы на 100 тысяч итераций.kroshanin
24.02.2016 20:52+9Зря вы так. HTML5 предоставляет широчайшие возможности в области работы с канвой. И в модулях отрисовки графики вполне вероятны "битвы" за каждую миллисекунду времени.
forgotten
24.02.2016 21:05+1Тут одно из двух. Или ты пользуешься готовой библиотекой, а клиентскую логику пишешь как удобнее и понятнее, а не как «производительнее».
Или ты сам разрабатываешь такую библиотеку, и тогда подобные советы у тебя вызывают только недоумении «как этого можно не знать».
Новичкам нужно запомнить ровно одно правило: не занимайся преждевременной оптимизацией.faiwer
24.02.2016 21:19+12Или ты сам разрабатываешь такую библиотеку, и тогда подобные советы у тебя вызывают только недоумении «как этого можно не знать».
Давайте без лишнего пафоса. Можно писать библиотеку или некую логику, которая будет вызываться и чаще чем миллион раз, и не знать таких вещей. В конце концов, когда говорят об экономии на спичках, обычно упоминают об этом, а не об new `Object vs {}`.
zim32
25.02.2016 00:56+3А вы не думали что функция которая возвращает indexOf из глобальной переменной могла просто заинлайниться, убрав в этом случае оверхед на лишних миллион созданий/вызовов внутренней функции?
ckr
25.02.2016 01:19+3Зашел на страницу статьи из-за горячего заголовка. Ожидал новую тру-практику. На деле же — просто разбор собственных полетов.
Во-первых, js-движку по барабану динамическая ли функция или не динамическая. По состоянию, важному для производительности, можно выделить откомпилированные функции и неоткомпилированные. Обычно функции компилируются при первом выполнении. Существуют и способы определения откомпилированных функций и без выполнения функции. Например, черезnew Function(..)
.
По поводу массивов. Вы правильно заметили, создание массива съедает немного производительности. Вы забыли учесть, что имеет место быть не менее трудоемкая задача — утилизация массива. В вашем же примере основная производительность тратится на постоянное изменение размера массива. По уму, размер массива надо задавать при создании. И, по возможности, следует пользоваться типизированными массивами.
Стоит также отметить, что ни в коем случае нельзя пользоваться большими массивами через замыкания или параметры вызова функций. Это переполняет стек процесса и создает немалую нагрузку на процессор.Sayonji
25.02.2016 01:46+1Как использование массивов через замыкания или аргументы влияет на стек процесса?
ckr
25.02.2016 03:13+1Подробнее здесь https://habrahabr.ru/company/plarium/blog/277129/
По поводу аргументов — у меня речь шла именно про большИе массивы. Разумеется, никто не запрещает передавать небольшие объекты/массивы, например, в качестве конфига. Дело в том, что в некоторых случаях использования массивов в качестве аргументов вызова функции, доступ к данным массива осуществляется не как через ссылку на исходный массив-объект, а происходит копирование массива и доступ к данным осуществляется уже к копии массива.Sayonji
25.02.2016 18:25Не подскажете, где почитать про копирование массива при подстановке в функцию?
gearbox
25.02.2016 18:49внимательно перечитал статью — не нашел там упоминания о передаче массива по значению. Вообще в моем понимании javascript этого не должно происходить ни при каком случае — объекты всегда передаются по ссылке, скаляры — по значению.
если я не прав — мое понимание javascript требует пересмотра с основ.
zxcabs
25.02.2016 23:11+1Вот так живешь живешь, а потом оказывается что в js массивы не по ссылкам передаются, а по значениям. Вы либо выразили свою мысль не правильно, либо несете что то из разряда фантастики.
ckr
27.02.2016 20:13Отвечу на один вопрос, заданный несколько раз выше, здесь.
Не помню точно, как дошел до этой практики. Уже тоже пруф найти не могу. Помню, дело было за долго до nodejs. Делали web-интерфейс на ExtJS для несложной но ёмкой БД. При активной передаче некоторых массивов описанным выше способом вешался весь браузер.
Сейчас похожее поведение может проявляться при вызовах из JS функций с кодом, например, написанных на Си.ckr
27.02.2016 20:26Кстати, справились с той проблемой так: вместо передачи массивов как аргументов, стали объявлять эти массивы как property у объектов-контроллеров, и доступ к ним в методах осуществлялся через this. Это давало значительный прирост производительности в ходе работы с приложением.
RomanYakimchuk
25.02.2016 10:20+2Конечно, оптимизация функций имеет место быть (в некоторых случаях она необходима), но в общем случае оптимизация архитектуры приложения даст вам намного больший прирост в производительности, чем чрезмерная оптимизация тела некоторых функций.
Статья интересная, но на практике это пригодится для узкого круга задач, и при условии что подходящего инструмента для решения задачи нет.
Если есть люди, которые столкнулись с такими задачами, отпишитесь в комментарии, пожалуйста.
mraleph
25.02.2016 18:33+4Нельзя делать выводы на основе простых измерений, надо хотя бы профилировать и пытаться понять, что же на самом-то деле происходит внутри. Иначе получаются неправильные выводы.
Дело здесь в следующем — создание функций, которые содержат в себе литералы (например, array literal или там object literal), это более тяжелая операция по сравнению с созданием функций, которые в себе литералов не содержат.
Если взять и просто сравнить два профиля, то все тайное становится явным
function find(val){ function index (value) { return [1, 2, 3].indexOf(value); } return index(val); }
8.29% 67 | LazyCompile:*InnerArrayIndexOf native array.js:1020 * 7.67% 62 | v8::internal::JSFunction::set_literals * 7.05% 57 | v8::internal::Factory::NewFunctionFromSharedFunctionInfo * 5.81% 47 | v8::internal::Factory::NewFunction * 4.58% 37 | v8::internal::Factory::New<v8::internal::JSFunction * 4.33% 35 | v8::internal::Runtime_NewClosure 4.21% 34 | Stub:FastCloneShallowArrayStub 4.08% 33 | v8::internal::Heap::AllocateRaw 3.34% 27 | LazyCompile:~index test.js:2 * 3.34% 27 | v8::internal::Factory::NewFunctionFromSharedFunctionInfo 3.34% 27 | Builtin:ArgumentsAdaptorTrampoline 3.09% 25 | v8::internal::Heap::Allocate * 3.09% 25 | v8::internal::SharedFunctionInfo::SearchOptimizedCodeMap
function foo() { return [1, 2, 3]; } function find(val){ function index (value) { return foo().indexOf(value); } return index(val); }
13.58% 58 | LazyCompile:*InnerArrayIndexOf native array.js:1020 9.82% 42 | Builtin:ArgumentsAdaptorTrampoline 7.49% 32 | LazyCompile:~index test1.js:6 7.01% 30 | LoadIC:A load IC from the snapshot * 6.08% 26 | Stub:FastNewClosureStub 5.62% 24 | Builtin:CallFunction_ReceiverIsNullOrUndefined 3.74% 16 | LazyCompile:*foo test1.js:1 3.51% 15 | Builtin:Call_ReceiverIsNullOrUndefined 3.51% 15 | LazyCompile:*indexOf native array.js:1065
В первом случае мы ходим много в среду исполнения и там занимаемся всякой тяжелой и малополезной работой (например, клонированием массива литералов привязанного к замыканию), а во втором случае мы быстренько создаем замыкание с помощьюFastNewClosureStub
. Вот отсюда и основная разница.
Akuma
> Многие заметят и скажут: будет такое же время. А не тут то было. Выдал 25. Хотя массивы создавались столько же раз.
Массивы что там, что здесь создаются только при выполнении этой функции.
А вот сама функция в случае ее выноса во вне, создается один раз, а когда вы объявляете ее в объекте — создается каждый раз.
А вообще, вы не открыли америку.
peacecoder85
Вы видимо невнимательно читали статью. Да она создается много кратно. И я вывел время ее создания. Это 20 мс/1000000 повторений, а вот массивы создаваемые в ней уже создаются разное время.