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

в 6:08, , рубрики: javascript, node.js, оптимизация, урок, метки: , , ,

Хочу поделится с вами очень интересным приемом оптимизации в Javascript.
Почему eval, это не всегда плохо
Давайте рассмотрим гипотетическую ситуацию. Допустим у нас есть стек серверов, которые, в любой момент времени, по своей внутренней прихоти, могут быть в состоянии «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 {};}
};

Выглядит дико, я с вами полностью согласен. Но оно работает!

Почему eval, это не всегда плохо
Давайте проверим, оправдал ли себя этот подход или нет. Напишем небольшую функцию которая добавит наблюдателю 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 на моем ноуте:
Почему eval, это не всегда плохо
Видите! Выжали 30% практически из пустого места!

Спасибо если вам было интересно.

Идею подхватил в одном из докладов на JSConf EU 2012.

Автор: idoroshenko

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js