Почему Object.create настолько медленнее, чем конструктор?

Фон

В проекте, который я поддерживаю, мы широко используем объекты нулевого прототипа как альтернативу бедному человеку (только для строкового ключа) Карты, которые не поддерживаются в большинстве старых браузеров до ES6.

В принципе, для создания нулевого объекта прототипа "на лету" можно использовать:

var foo = Object.create(null);

Это гарантирует, что новый объект не имеет наследуемых свойств, таких как "toString", "constructor", "__proto__", которые нежелательны для данного конкретного варианта использования.

Поскольку этот шаблон появляется несколько раз в коде, мы придумали идею создания конструктора, который будет создавать объекты, у прототипа которых есть нулевой прототип и нет собственных свойств.

var Empty = function () { };
Empty.prototype = Object.create(null);

Затем для создания объекта без собственных или унаследованных свойств можно использовать:

var bar = new Empty;

Проблема

В стремлении повысить производительность я написал тест и обнаружил, что нативный подход Object.create неожиданно выполняет гораздо медленнее, чем метод, включающий дополнительный конструктор со специальным прототипом во всех браузерах: http://jsperf.com/blank-object-creation.

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

Что может быть причиной такой разницы в производительности?

Ответ 1

Вы изучали что-то, что сильно зависит от конкретной версии браузера, в котором вы работаете. Вот некоторые результаты, которые я получаю здесь, когда я запускаю ваш тест jsperf:

  • В Chrome 47 new Empty работает с частотой 63 м операц./сек, тогда как Object.create(null) работает со скоростью 10 м операц./сек.

  • В Firefox 39 new Empty работает со скоростью 733 м операц./сек, тогда как Object.create(null) работает на 1,685 м операционных/сек.

( "m" выше означает, что мы говорим о миллионах.)

Итак, какой из них вы выбираете? Самый быстрый метод в одном браузере самый медленный в другом.

Не только это, но результаты, которые мы здесь просматриваем, с большой вероятностью могут измениться с новыми версиями браузера. В данном случае я проверил реализацию Object.create в v8. До 30 декабря 2015 года реализация Object.create была написана на JavaScript, но commit недавно изменил ее на реализацию на С++. Как только это пробивается в Chrome, результаты сравнения Object.create(null) и new Empty будут меняться.

Но это еще не все...

Вы рассмотрели только один аспект использования Object.create(null) для создания объекта, который будет использоваться как вид карты (псевдоарма). Как насчет времени доступа к этой псевдокарте? Вот тест, который проверяет производительность misses и проверяет производительность hits.

  • В Chrome 47 оба случая и промахов на 90% быстрее с объектом, созданным с помощью Object.create(null).

  • В Firefox 39 все случаи выполнения все одинаковы. Что касается случаев промаха, производительность объекта, созданного с помощью Object.create(null), настолько велика, что jsperf сообщает мне, что число ops/sec равно "Бесконечность".

Результаты, полученные с Firefox 39, - это те, которые я действительно ожидал. Механизм JavaScript должен искать поле в самом объекте. Если это удар, то поиск завершен, независимо от того, как был создан объект. Если есть недостаток в поиске поля в самом объекте, то механизм JavaScript должен проверить прототип объекта. В случае объектов, созданных с помощью Object.create(null), прототипа нет, поэтому поиск заканчивается. В случае объектов, созданных с помощью new Empty, существует прототип, в котором движок JavaScript должен искать.

Теперь, в течение жизни псевдо-карты, как часто создается псевдо-карта? Как часто к нему обращаются? Если вы не находитесь в действительно необычной ситуации, карта должна быть создана один раз, но доступна много раз. Таким образом, относительная производительность просмотров и пропусков будет более важной для общей производительности вашего приложения, а затем относительной производительности различных средств создания объекта.

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

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

Ответ 2

Разница в производительности связана с тем, что функции конструктора сильно оптимизированы в большинстве JS-движков. На самом деле нет практической причины, по которой Object.create не может быть так же быстро, как функции-конструкторы, это просто зависящая от реализации вещь, которая со временем будет улучшаться.

Таким образом, все тесты производительности доказывают, что вы не должны выбирать тот или иной на основе производительности, потому что стоимость создания объекта является смехотворно низкой. Сколько из этих карт вы создаете? Даже самая медленная реализация Object.create в тестах по-прежнему прерывает более 8 000 000 объектов в секунду, поэтому, если у вас нет веских причин для создания миллионов карт, я бы выбрал наиболее очевидное решение.

Кроме того, рассмотрим тот факт, что одна реализация браузера может буквально быть в 100 раз быстрее, чем другая реализация. Эта разница будет существовать независимо от того, какой вы выбираете, поэтому небольшая разница между Object.create и конструкторами не должна считаться существенным различием в более широком контексте различных реализаций.

В конечном итоге Object.create(null) является очевидным решением. Если производительность создания объектов становится узким местом, то, возможно, стоит рассмотреть возможность использования конструкторов, но даже тогда я бы посмотрел в другом месте, прежде чем прибегать к использованию чего-то вроде конструкторов Empty.

Ответ 3

Этот вопрос в значительной степени недействителен, потому что jsperf сломан, он искажает результаты по любой причине. Я проверил его лично, когда я делал свою собственную реализацию карты (основанную на целых числах).

Между этими двумя методами нет никакой разницы.

BTW Я думаю, что это более простой способ создать пустой объект с тем же синтаксисом:

var EmptyV2 = function() { return Object.create(null); };

Я написал свой собственный собственный тест, который печатает время для создания любого количества этих 3 методов.

Вот он:

<!DOCTYPE html>
<html>
    <head>
        <style>
            html
            {
                background-color: #111111;
                color: #2ECC40;
            }
        </style>
    </head>
    <body>
    <div id="output">

    </div>

    <script type="text/javascript">
        var Empty = function(){};
        Empty.prototype = Object.create(null);

        var EmptyV2 = function() { return Object.create(null); };

        var objectCreate = Object.create;

        function createEmpties(iterations)
        {           
            for(var i = 0; i < iterations; i++)
            {           
                var empty = new Empty();
            }
        }

        function createEmptiesV2(iterations)
        {       
            for(var i = 0; i < iterations; i++)
            {
                var empty = new EmptyV2();
            }
        }

        function createNullObjects(iterations)
        {       
            for(var i = 0; i < iterations; i++)
            {
                var empty = objectCreate(null);
            }
        }

        function addResult(name, start, end, time)
        {           
            var outputBlock = document.getElementsByClassName("output-block");

            var length = (!outputBlock ? 0 : outputBlock.length) + 1;
            var index = length % 3;

            console.log(length);
            console.log(index);

            var output = document.createElement("div");
            output.setAttribute("class", "output-block");
            output.setAttribute("id", ["output-block-", index].join(''));
            output.innerHTML = ["|", name, "|", " started: ", start, " --- ended: ", end, " --- time: ", time].join('');

            document.getElementById("output").appendChild(output);

            if(!index)
            {
                var hr = document.createElement("hr");
                document.getElementById("output").appendChild(hr);
            }
        }

        function runTest(test, iterations)
        {
            var start = new Date().getTime();

            test(iterations);

            var end = new Date().getTime();

            addResult(test.name, start, end, end - start);
        }

        function runTests(tests, iterations)
        {
            if(!tests.length)
            {
                if(!iterations)
                {
                    return;
                }

                console.log(iterations);

                iterations--;

                original = [createEmpties, createEmptiesV2, createNullObjects];

                var tests = [];

                for(var i = 0; i < original.length; i++)
                {
                    tests.push(original[i]);
                }
            }

            runTest(tests[0], 10000000000/8);

            tests.shift();

            setTimeout(runTests, 100, tests, iterations);
        }

        runTests([], 10);
    </script>
    </body>
</html>

Прошу прощения, это немного жестко. Просто вставьте его в index.html и запустите. Я думаю, что этот метод тестирования намного превосходит jsperf.

Вот мои результаты:

| createEmpties | начало: 1451996562280 --- закончился: 1451996563073 --- время: 793
| CreateEmptiesV2 | начало: 1451996563181 --- закончился: 1451996564033 --- раз: 852
| CreateNullObjects | начал: 1451996564148 --- закончился: 1451996564980 --- раз: 832


| CreateEmpties | начало: 1451996565085 --- закончился: 1451996565926 --- раз: 841
| CreateEmptiesV2 | начато: 1451996566035 --- закончился: 1451996566863 --- раз: 828
| CreateNullObjects | начал: 1451996566980 --- закончился: 1451996567872 --- раз: 892

| CreateEmpties | начало: 1451996567986 --- закончился: 1451996568839 --- время: 853
| CreateEmptiesV2 | начало: 1451996568953 --- закончился: 1451996569786 --- раз: 833
| CreateNullObjects | начал: 1451996569890 --- закончился: 1451996570713 --- раз: 823

| CreateEmpties | начало: 1451996570825 --- закончился: 1451996571666 --- раз: 841
| CreateEmptiesV2 | начало: 1451996571776 --- закончился: 1451996572615 --- раз: 839
| CreateNullObjects | начал: 1451996572728 --- закончился: 1451996573556 --- время: 828

| CreateEmpties | начало: 1451996573665 --- закончился: 1451996574533 --- время: 868
| CreateEmptiesV2 | начал: 1451996574646 --- закончился: 1451996575476 --- время: 830
| CreateNullObjects | начал: 1451996575582 --- закончился: 1451996576427 --- время: 845

| CreateEmpties | начало: 1451996576535 --- закончился: 1451996577361 --- раз: 826
| CreateEmptiesV2 | начал: 1451996577470 --- закончился: 1451996578317 --- время: 847
| CreateNullObjects | начал: 1451996578422 --- закончился: 1451996579256 --- раз: 834

| CreateEmpties | начало: 1451996579358 --- закончился: 1451996580187 --- раз: 829
| CreateEmptiesV2 | начало: 1451996580293 --- закончился: 1451996581148 --- время: 855
| CreateNullObjects | начал: 1451996581261 --- закончился: 1451996582098 --- время: 837

| CreateEmpties | начало: 1451996582213 --- закончился: 1451996583071 --- время: 858
| CreateEmptiesV2 | начал: 1451996583179 --- закончился: 1451996583991 --- время: 812
| CreateNullObjects | начато: 1451996584100 --- закончилось: 1451996584948 --- раз: 848

| CreateEmpties | начало: 1451996585052 --- закончился: 1451996585888 --- время: 836
| CreateEmptiesV2 | начал: 1451996586003 --- закончился: 1451996586839 --- время: 836
| CreateNullObjects | начал: 1451996586954 --- закончился: 1451996587785 --- раз: 831

| CreateEmpties | начало: 1451996587891 --- закончился: 1451996588754 --- время: 863
| CreateEmptiesV2 | начал: 1451996588858 --- закончился: 1451996589702 --- время: 844
| CreateNullObjects | начато: 1451996589810 --- закончился: 1451996590640 --- раз: 830

Ответ 4

В стремлении повысить производительность я написал тест и обнаружил, что родной метод Object.create неожиданно выполняет гораздо медленнее, чем метод, включающий дополнительный конструктор с специальным прототипом, в все браузеры

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

В ваших рассуждениях постулируется, что оператор new и Object.create должны использовать один и тот же внутренний код создания объекта, с дополнительным вызовом настраиваемого конструктора для new. Вот почему вы находите результат теста неожиданным, потому что вы думаете, что сравниваете A + B с A.

Но это не так, вы не должны так много думать о реализациях new и Object.create. Оба могут разрешать разные JS или "native" (в основном С++), и ваш пользовательский конструктор может быть легко оптимизирован синтаксическим анализатором.

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

Если вы действительно беспокоитесь о времени создания объекта, добавьте счетчик количества созданных объектов, увеличьте его в своем конструкторе Empty, запишите количество объектов, созданных в течение жизни программы, умножьте на самое медленное время выполнения браузера и посмотрите (наиболее вероятно), насколько незначительно время создания.