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

    Хочу поделится с вами очень интересным приемом оптимизации в 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 может на ходу оптимизировать свой код, для достижения лучшей производительности.

    Доклад из которого я взял этот подход.
    Его текстовая версия.
    Поделиться публикацией

    Комментарии 55

      +26
      Проблема только в том, что в реальных условиях Server.ping будет выполняться на 50000% медленнее и эти 30%, изрядно подпортившие читаемость кода, будут никому не нужны
        +12
        Не говоря уже о том, Server.ping — это операция, которая большую часть времени ждет. Правильная оптимизация цикла — распараллелить его.
          –1
          Не то чтобы «ждет». Это JavaScript и такие операции обычно асинхронные.

          Хотя кто его знает что за Server.ping кроется.
            0
            Судя по изначальной форме цикла, операция все-таки синхронная.
              0
              Вот именно это меня в этом коде и смущает :) Я подозреваю что подобные вещи нужны чтобы динамически статус подключения показывать и если этот код синхронный и «повиснет», повесив интерфейс, пока от серверов ответов не дождется то это плохо.
          +3
          По словам разработчика одного из самых быстрых MySQL driver'ов для Node.js, этот подход помог ему выиграть эти же 30% при парсинге полей в ответах с сервера.
          Естественно этот метод далеко не панацея, и всё очень сильно зависит от ситуации.
            0
            Вполне может быть. Создавать объект eval-ом по записям БД, полученным сервера — не такая плохая идея, по сравнению с перебором полей в цикле. Особенно результат запроса «широкий» и записей много.

            Но в коде вашего примера меня более всего смущает синхронный пинг :)
              +1
              Всех смущает не пинг, а название метода. Нужно было назвать его ".isOk()" :P
                0
                Возможно :)
            +4
            Не JS но:
            p = Net::Ping::External.new('195.54.2.1')
            
            Benchmark.bm do |x|
              x.report {100.times do;p.ping;end}
            end
            

            user system total real
            0.100000 0.220000 0.320000 ( 1.258235)

            1.25 секунд на 100 пингов в 1 поток.
            Итого Server.ping будет выполняться примерно 80 пингов в секунду
            Всего в 805,6625 раз медленнее чем 64453, а это соответствует 80566,25%
            Ваши расчеты не верны :)
              +4
              Зануда! (это комплимент)
                0
                Надо еще IPшник в двоичном формате передавать. Вы хоть представляете сколько времени строчка парсится?
                  0
                  '195.54.2.1'.split('').map(&:ord).map{|i| i.to_s(2)}
                  => [«110001», «111001», «110101», «101110», «110101», «110100», «101110», «110010», «101110», «110001»]? :)

                    0
                    Простите мою неграмотность, а что за язык? Ruby?
                      0
                      Ога, только я не считаю это «неграмотностью». Ruby входит в большую группу интерпретируемых ООП языков, умение отличать один от другого, по моему, бесполезный навык.
                        0
                        Просто синтаксис довольно интересен, особенно конструкции вида "&:ord"
                          0
                          Унарный амперсанд чудесен, дааа…
                          habrahabr.ru/post/111722/
                          Ruby вообще богат чудесными идиоматическими приемами, редко встречающимся в программировании.

                          User.first do |user|
                          … туткакойтакод
                          end.attributes.values
                          выдаст значение аттрибутов
                          но
                          применение методов к оператору закрытия блока разнесло мне мозг вдребезги при первой
              +10
              Собственно, eval — зло только когда вы включаете в него данные, которые не контролируете — пользовательский ввод и так далее.
                0
                не только. Еще он сильно мешает оптимизации JS-кода по размеру для сложных веб-приложений: их не получается автоматически просчитать на область видимости переменных
                  –1
                  Согласен. Но если в большом приложении существенную долю занимает eval, то проблемы начнутся задолго до минификации файлов.
                +19
                Уже предвижу, как некоторые новички побегут писать код при помощи eval, потому что «я читал на хабре, что он так будет быстрее работать».
                Я бы такую технику отнёс к разряду экспертных, и использовать ее можно только когда вы точно знаете, что вам это нужно, и понимаете ВСЕ плюсы и минусы, а также имеете тесты и бенчмарки, чтобы проверить, что оно действительно помогло.
                  +3
                  Также есть замечания по самому бенчмарку:
                  1) Date.now() — заметно быстрее, чем new Date().getTime()
                  2) Вычислять дату на каждую итерацию — накладно. Лучше делать это для тысяч итераций, чтобы минимально вносить погрешности.
                  У меня получилось так:
                      var stamp = Date.now();
                      var iterations = 0;
                      while(Date.now() - stamp <= 1000){
                          for (var i = 0; i < 1000; i++) {
                              instance.check();
                          }
                          iterations += 1000;
                      }
                      console.log((iterations * 1000 / (Date.now() - stamp)).toFixed(2) + ' iterations per second for ' + note);
                  

                  Соотв-но, в Вашей версии скорость была
                  449194 iterations per second for self
                  80822 iterations per second for casual
                  

                  в моей:
                  738261.74 iterations per second for self
                  91179.39 iterations per second for casual
                  

                  это на ubuntu 11.04 + node.js 0.8.14
                    +1
                    а еще можно вынести Server.ping в Server.prototype.ping и скорости будут 1 411 588 и 109 126, соответственно.
                      0
                      Спасибо за правку. Изначально скорость мерил у себя на живом проекте, а бенчамарк метод накидал специально для этого поста. Потом конечно спохватился по некоторым моментам, но посчитал что уже поздно, тем более основной посыл был в другом, и вы всё идеально расписали.
                      0
                      Идею подхватил в одном из докладов на JSConf EU 2012.

                      Ссылочки не найдётся?
                      –1
                      А что в JS циклы настолько тормозят? Генеренный код точно так же словарь заполняет, единственная разница что нет цикла.

                      А если тоже самое сделать на замыканиях?
                        –1
                        Проблема — не в тормозящем цикле, а в том, что тело цикла ничего не делает.
                        +6
                        Если внимательно смотреть его доклад, то он явно говорит, что «Рад бы не использовать этот костыль потому как это очень не явный метод оптимизации» Да и он сам оптимизировал свой код экспериментально(алхимия), а не на основе структурированных знаний о V8 (химия). И в этом, конечно, нет ничего плохого потому, что все знать нельзя.

                        Статическое наполнение объекта быстрее динамического:
                        function (names, fields) {
                            var result = {};
                        
                            for (var i = 0, c = names.length; i < c; i++) {
                                result[names[i]] = fields.fetch();
                            }
                        
                            return result;
                        }

                        Вот этот код будет работать быстрее
                        function (names, fields) {
                            return {
                                "name1": fields.fetch(),
                                "name2": fields.fetch(),
                                "name3": fields.fetch()
                            };
                        }

                        потому как (если я не ошибаюсь) в этом месте V8 в качестве представления объектов JavaScript будет использовать скрытые классы, а в первом случае словарь. // cc mraleph

                        В 95% случаев вам не нужны оптимизации, в 4.5% случаев вас спасает JIT, а вот только в 0.5% случаев необходимо использовать вот такое извращение, притом не абы где, а на основе испытаний.

                        Используйте eval с умом! Он плохо себя ведет только в не умелых руках, поэтому его и не советуют использовать ;-) Ну и не забывайте о преждевременной оптимизации
                          –1
                          он сам оптимизировал свой код экспериментально(алхимия), а не на основе структурированных знаний о V8 (химия)
                          Это как раз то что подвигло меня поделится этим подходом со всеми. Если об этом можно было прочитать в методичке, я бы хмыкнул, и продолжил бы листать :)
                          Кстати, именно таким подходом, я обнаружил что, например в той же мозиле, eval будет работать быстрее чем new Function, так что для наглядности решил оставить его.
                          Хотя, в моем коде на Node.js, я использовал исключительно new Function подход.
                            –1
                            Какой из 2-х eval-ов? Контекста функции или глобальный?
                            +5
                            скрытые классы всегда используются. словарное представление тоже имеет скрытый класс, который собственно и говорит что поля представлены словарем.

                            превратится-ли result в словарь зависит от количества полей и некоторых других факторов.

                            основное ускорение здесь получается от того, что вы полустатическим образом фиксируете структуру литерала в коде, не нужно ничего растить и т.д., то, что он не перейдет в словарь дополнительная плюшка из этого.

                            ну и eval'а на V8 нужно избегать как огня — произведенный им код не оптимизируется. хотя в данном конкретном случае это и не важно, потому что основное время тратится в клонировании object literal boilerplate, которое выполняется большей частью в стабе.
                              0
                              > произведенный им код не оптимизируется

                              Я когда то уже спрашивал, но переспрошу еще раз. Во всех случаях? Или «хороший» eval сработает?

                              new Function('global', 'global.alert(global.localStorage.item);')(window);
                              

                              Наружу ничего не лезет, все работает в том же скоупе.
                                +2
                                Никакой произведенный evalом код не оптимизируется. Но new Function — не eval, поэтому произведенный им код оптимизируется.
                            +1
                            С тем-же успехом, и для тех же целей, можно использовать всеми ненавистный eval.

                            n64js.blogspot.ru/2012/09/another-reason-to-avoid-eval.html
                              +1
                              [зануда-мод]
                              Почему сначала вы приводите пример с new Function, а в результате используете eval? Это не одно и то же.

                              В вашем примере с одинаковым успехом можно использовать оба способа, но new Function более предпочтительней из-за соображений той же оптимизации. Код внутри созданной функции подвергается внутренним оптимизациям в компиляторе, а в eval — нет.
                              Конечно, всё зависит от конкретной ситуации и, возможно, здесь это ничего не даст.

                              Это я к тому, что, главная тема поста не раскрыта — здесь не eval хорошо, а сам способ нужно знать и уметь применять. Ну и eval здесь не самый подходящий инструмент.

                              Про реализацию конструкторов я вообще молчу.
                              [/зануда-мод]
                                –1
                                Прочитал комментарий выше, первый вопрос снят :)
                                  –1
                                  Выше уже писал, как оказалось, в той же мозиле, прирост с new Function оказался почти не заметным, даже чуть ли не отрицательным. В данном посте решил, для наглядности, использовать eval который более привычен широкому кругу пользователей JS. Но намеренно упомянул возможность использования new Function.
                                  Жалею, что не был более информативным по всем этим моментам в посте, так что очень рад таким замечательным правкам в комментариях.
                                  –1
                                  А какие еще есть применения у подобного eval, с озвученными плюсами, кроме создания объекта без итерирования? Я что-то не могу придумать.
                                    +1
                                    Шаблоны
                                    Транслировавшие CoffeeScript в JavaScript на лету
                                      0
                                      gist.github.com/4130117
                                      интересно в случаях, когда структура клонируемого объекта известна заранее
                                      дял теста либо нужен lodash/underscore.js, либо можно удалить секцию.
                                        +1
                                        сделал небольшой модуль:
                                        https://groups.google.com/forum/?fromgroups=#!topic/nodejs/oRQgLeCDxbA :)
                                          0
                                          Ого, серьезный инкрейс. Не ожидал.
                                            0
                                            Ну описанный тут метод все равно заруливает clone из v8. Но по сравнению с ним, применимость очень ограничена.
                                            0
                                            Надо допилить и сделать еще «глубокий» клонер по шаблону.
                                              0
                                              В каком смысле «по шаблону»?
                                                0
                                                new Cloner.byTemplate({a:{c:{}},b:""}).clone();
                                                
                                                  0
                                                  Ну смысл модуля совсем в другом, и не имеет прямого отношения к этой статье.
                                        –1
                                        Да это хэш и создавать-то не обязательно — достаточно сварганить прокси:

                                        var LazyObserver = function( ){
                                        var hashTable= { }
                                        this.add= function( server ){
                                        hashTable.__defineGetter__( server.name, function( ){
                                        return server.ping()
                                        })
                                        return this
                                        }
                                        this.check= function( ){
                                        return hashTable
                                        }
                                        }
                                          –1
                                          Откуда этот фанатизм? Я о выдавливании из себя критических комментариев. Тут есть действительно отличные замечания, я даже поблагодарил за них и проплюсовал. Но иногда бывают бессмысленные замечания, чуть ли не на уровне — «В JS это не нужно!». Вы ведь, я полагаю, понимаете, что ваша реализация тут не уместна. Скрипт занимается мониторингом серверов, и если, например, нужно сделать 1000 снапшотов состояний серверов за определенный промежуток времени, такая реализация, по крайней мере в таком виде, будет совсем не уместна.
                                          И это естественно, ведь для демонстрации этого приёма, я подогнал не реализацию под ситуацию, а наоборот. Может, и не совсем успешно.
                                          –1
                                          Eval — зло. Это утверждение можно принять на веру. Но если подумать…
                                          С другой стороны не всегда есть время подумать.
                                            +2
                                            Если нет времени подумать, то eval — зло.
                                            –3
                                            Фееричная ерунда, уж простите за откровенность.

                                            Мало того, что экономите на спичках, так еще и вешаете на стену ружжо, которое обязательно станцует и споет, как это принято по законом индийского кино :)
                                            Eval — зло, with — зло. Если хочешь быть приличным командным кодером — это догматы.

                                            Кроме того, JIT весьма обидчив, и если, вымахиваясь, вы его перехитрите, то он на вам обидется и оставит без сладкого, в одиночестве, с неоптимизированным кодом.

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                            Самое читаемое