Хочу поделится с вами очень интересным приемом оптимизации в Javascript.
Давайте рассмотрим гипотетическую ситуацию. Допустим у нас есть стек серверов, которые, в любой момент времени, по своей внутренней прихоти, могут быть в состоянии «ok» или «down». Интерфейс сервера позволяет узнать только его имя и текущее состояние. Но как устроен сервер, и откуда он берется нам не известно, у нас нет к нему доступа. Пусть этот код будет конструктором для наших серверов:
var Server = function(name){
this.name = name;
this.ping = function(){return Math.round(Math.random())? 'ok' : 'down';};
};
Допустим есть компонент, наблюдатель, который будет следить за вверенными ему серверами, и по первому требованию отдавать нам хеш таблицу состояний по всем серверам. А какой-то демон, постоянно, яростно теребит этот компонент, следя за состоянием серверов. Пусть это будет конструктор нашего наблюдателя:
var CasualObserver = function(){
var stack = [];
this.add = function(server){
stack.push(server);
return this;
};
this.check = function(){
var hashTable = {};
for(var i = 0, ln = stack.length; i < ln; i++){
hashTable[stack[i].name] = stack[i].ping();
}
return hashTable;
}
};
Допустим вы программист которому поручили этот код, и, вежливо, попросили его оптимизировать, причем сохранив существующий интерфейс.
Ну вот сидите вы, и пялитесь в эти 14 строчек, и единственная (я иронизирую) мысль которая вертится у вас в голове — «стоит ли for на while заменить, или всё-же не стоит?». Мысль эта одновременно правильная и неправильная (эдакая суперпозиция мысли). Правильная в том, что самая трудоёмкая операция тут — цикл. Неправильная в том, что нужно думать не о том как оптимизировать его, а о том как избавится от него. Зачем нам динамически создавать хеш таблицу, когда можно работать с уже готовой, и просто вызывать .check для каждого элемента?
this.check = function(){
return {
stack[0].name : stack[0].ping(),
stack[1].name : stack[1].ping(),
stack[2].name : stack[2].ping()
};
}
Вот так мы сразу же избавляемся от цикла. Конечно вы посмотрите на меня как на дурака, и скажите — «Ага, Вань. Но серверов то у нас может быть произвольное количество, а не просто три. Да и к тому-же в JS нет рефлекшенов.»
А вы знали что в Javascript возможна примитивная рефлексия? (знали? Ну тогда ступайте ниже) Да-да, JS позволяет изменять существующий код и создавать новый в прямо в рантайме!
var sum = new Function('a', 'b', 'return alert(a + b);');
sum(2, 3);
С тем-же успехом, и для тех же целей, можно использовать всеми ненавистный eval. Подробнее об этом уже писали в других постах на хабре.
Ага, вы неверное уже всё сами поняли? Конечно! Давайте создадим конструктор для нашего наблюдателя, который сам для себя создать метод check:
var SelfModifyObserver = function(){
var stack = [];
this.add = function(server){
stack.push(server);
var code = 'return {';
for(var i = 0, ln = stack.length; i < ln; i++){
code += stack[i].name + ':' + 'stack[' + i + '].ping(),';
}
code += '};';
this.check = eval('(function(){' + code +'});');
return this;
};
this.check = function(){return {};}
};
Выглядит дико, я с вами полностью согласен. Но оно работает!
Давайте проверим, оправдал ли себя этот подход или нет. Напишем небольшую функцию которая добавит наблюдателю 25 серверов, по одному на каждую букву латинского алфавита, а потом посмотрим сколько раз за секунду он сможет сделать проверку по ним.
var benchmark = function(instance, note){
for(var i=65; i<=90; i++){
instance.add(new Server(String.fromCharCode(i)));
}
var stamp = new Date().getTime();
var iterations = 0;
while(new Date().getTime() - stamp <= 1000){
instance.check();
iterations++;
}
console.log(iterations + ' iterations per second for ' + note);
return true;
};
Вынужден признать, что сейчас у меня нет возможности протестировать это в Node.js, но тесты в офисе показали что на моем сервере прирост в производительности для подобного решения был порядка 30%.
А это результат в chrome на моем ноуте:
Видите! Выжали 30% практически из пустого места!
Спасибо если вам было интересно.
Идею подхватил в одном из докладов на JSConf EU 2012.
Автор: idoroshenko