Pull to refresh

Почему eval — это не всегда плохо

JavaScript *Node.JS *
Хочу поделится с вами очень интересным приемом оптимизации в 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.

Итого: пост не о том как правильно писать обсервер для наблюдения за другими объектами, и не о том каким способом можно избавиться от циклов. А о том, как javascript может на ходу оптимизировать свой код, для достижения лучшей производительности.

Доклад из которого я взял этот подход.
Его текстовая версия.
Tags:
Hubs:
Total votes 64: ↑44 and ↓20 +24
Views 21K
Comments Comments 55