Подтвердить что ты не робот

Почему доступ к переменной с помощью window.variable медленнее?

Несколько источников для JS-рекомендаций по производительности побуждают разработчиков сократить "поиск по цепочке цепей". Например, IIFE рекламируются как имеющие бонусное преимущество "уменьшения поиска цепочки объектов" при доступе к глобальным переменным. Это звучит вполне логично, возможно, даже воспринимается как должное, поэтому я не стал подвергать сомнению мудрость. Как и многие другие, я с радостью использовал IIFE, думая, что помимо предотвращения глобального загрязнения пространства имен, это будет повышение производительности по любому глобальному коду.

Что мы ожидаем сегодня:

(function($, window, undefined) {
    // apparently, variable access here is faster than outside the IIFE
})(jQuery, window);

Упрощая/распространяя это на обобщенный случай, можно было бы ожидать:

var x = 0;
(function(window) {
    // accessing window.x here should be faster
})(window);

Основываясь на моем понимании JS, в глобальной области нет разницы между x = 1; и window.x = 1;. Поэтому логично ожидать, что они будут одинаково результативными, не так ли? НЕПРАВИЛЬНО. Я провел несколько тестов и обнаружил существенную разницу в времени доступа.

Хорошо, возможно, если я поместил window.x = 1; внутри IIFE, он должен работать еще быстрее (хотя бы немного), правильно? НЕПРАВИЛЬНО.

Хорошо, может быть, это Firefox; попробуйте Chrome вместо этого (V8 является эталоном скорости JS, да?) Он должен бить Firefox для простых вещей, таких как доступ к глобальной переменной напрямую, не так ли? НЕПРАВИЛЬНО еще раз.

Итак, я решил выяснить, какой именно метод доступа является самым быстрым, в каждом из двух браузеров. Итак, скажем, мы начинаем с одной строки кода: var x = 0;. После того, как x был объявлен (и с радостью присоединен к window), какой из этих методов доступа будет самым быстрым и почему?

  • Непосредственно в глобальной области

    x = x + 1;
    
  • Непосредственно в глобальной области, но с префиксом window

    window.x = window.x + 1;
    
  • Внутри функции неквалифицирован

    function accessUnqualified() {
        x = x + 1;
    }
    
  • Внутри функции с префиксом window

    function accessWindowPrefix() {
        window.x = window.x + 1;
    }
    
  • Внутри функции, окно кеша как переменная, префикс доступа (имитирует локальный параметр IIFE).

    function accessCacheWindow() {
        var global = window;
        global.x = global.x + 1;
    }
    
  • Внутри IIFE (окно как параметр), префикс доступа.

     (function(global){
         global.x = global.x + 1;
     })(window);
    
  • Внутри IIFE (окно как параметр), неквалифицированный доступ.

     (function(global){
         x = x + 1;
     })(window);
    

Предположим, что контекст браузера, т.е. window - глобальная переменная.

Я написал быстрый тест времени, чтобы цикл операции увеличения в миллион раз, и был удивлен результатами. Что я нашел:

                             Firefox          Chrome
                             -------          ------
1. Direct access             848ms            1757ms
2. Direct window.x           2352ms           2377ms
3. in function, x            338ms            3ms
4. in function, window.x     1752ms           835ms
5. simulate IIFE global.x    786ms            10ms
6. IIFE, global.x            791ms            11ms
7. IIFE, x                   331ms            655ms

Я повторил тест несколько раз, и цифры кажутся показательными. Но они меня смущают, поскольку они, кажется, предлагают:

  • префикс с window намного медленнее (# 2 vs # 1, # 4 vs # 3). Но ПОЧЕМУ?
  • доступ к глобальной функции (возможно, дополнительный просмотр области) быстрее (# 3 vs # 1). Почему??
  • Почему результаты # 5, # 6, # 7 отличаются друг от друга в двух браузерах?

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

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


Изменить: Совместное использование моего тестового кода в соответствии с запросом.

var x, startTime, endTime, time;

// Test #1: x
x = 0;
startTime = Date.now();
for (var i=0; i<1000000; i++) {
   x = x + 1;
}
endTime = Date.now();
time = endTime - startTime;
console.log('access x directly    - Completed in ' + time + 'ms');

// Test #2: window.x
x = 0;
startTime = Date.now();
for (var i=0; i<1000000; i++) {
  window.x = window.x + 1;
}
endTime = Date.now();
time = endTime - startTime;
console.log('access window.x     - Completed in ' + time + 'ms');

// Test #3: inside function, x
x =0;
startTime = Date.now();
accessUnqualified();
endTime = Date.now();
time = endTime - startTime;
console.log('accessUnqualified() - Completed in ' + time + 'ms');

// Test #4: inside function, window.x
x =0;
startTime = Date.now();
accessWindowPrefix();
endTime = Date.now();
time = endTime - startTime;
console.log('accessWindowPrefix()- Completed in ' + time + 'ms');

// Test #5: function cache window (simulte IIFE), global.x
x =0;
startTime = Date.now();
accessCacheWindow();
endTime = Date.now();
time = endTime - startTime;
console.log('accessCacheWindow() - Completed in ' + time + 'ms');

// Test #6: IIFE, window.x
x = 0;
startTime = Date.now();
(function(window){
  for (var i=0; i<1000000; i++) {
    window.x = window.x+1;
  }
})(window);
endTime = Date.now();
time = endTime - startTime;
console.log('access IIFE window  - Completed in ' + time + 'ms');

// Test #7: IIFE x
x = 0;
startTime = Date.now();
(function(global){
  for (var i=0; i<1000000; i++) {
    x = x+1;
  }
})(window);
endTime = Date.now();
time = endTime - startTime;
console.log('access IIFE x      - Completed in ' + time + 'ms');


function accessUnqualified() {
  for (var i=0; i<1000000; i++) {
    x = x+1;
  }
}

function accessWindowPrefix() {
  for (var i=0; i<1000000; i++) {
    window.x = window.x+1;
  }
}

function accessCacheWindow() {
  var global = window;
  for (var i=0; i<1000000; i++) {
    global.x = global.x+1;
  }
}
4b9b3361

Ответ 1

Javascript ужасен для оптимизации из-за eval (который может получить доступ к локальному фрейму!).

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

Если у вас есть только локальные переменные, захваченные переменные и глобальные переменные, и если вы не можете предположить, что беспорядок с помощью eval выполняется, теоретически:

  • Локальный доступ к переменной - это просто прямой доступ в памяти со смещением от локального фрейма
  • Доступ к глобальной переменной - это просто прямой доступ в память
  • Для захваченного доступа к переменной требуется двойное косвенное обозначение

Причина в том, что если x при поиске результатов приводит к локальному или глобальному, тогда он всегда будет локальным или глобальным, и, следовательно, к нему можно будет напрямую обратиться с помощью mov rax, [rbp+0x12] (при локальном) или mov rax, [rip+0x12345678], когда глобальный. Нет никакого поиска.

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

mov rax, [rbp]      ; Load closure data address in rax
mov rax, [rax+0x12] ; Load cell address in rax
mov rax, [rax]      ; Load actual value of captured var in rax

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

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

Конечно, доступ к глобальному с использованием объекта window - это еще одно дело... и я не очень удивлен, что для этого требуется больше времени (window также должен быть обычным объектом).

Ответ 2

Следует отметить, что тестирование микро-оптимизаций уже не так просто, потому что JIT-JIT-компилятор оптимизирует код. Некоторые из ваших тестов, которые имеют чрезвычайно малые времена, вероятно, связаны с удалением компилятора "неиспользуемого" кода и циклов разворота.

Итак, действительно есть две вещи, которые нужно беспокоиться о "поиске в цепочке" и код, который препятствует компиляции JIT для компиляции или упрощения кода. (Последний очень сложный, поэтому вам лучше всего прочитать несколько советов и оставить его на этом.)

Проблема с цепочкой областей видимости заключается в том, что когда JS-движок встречает переменную типа x, ей необходимо определить, находится ли это:

  • локальная область
  • (например, созданные IIFE)
  • глобальная область

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

В следующем примере кода console.log(a); сначала пытается разрешить a в локальной области внутри innerFunc(). Он не находит локальную переменную a, поэтому она выглядит в закрывающем закрытии, а также не находит переменную a. (Если бы были добавлены дополнительные вложенные обратные вызовы, вызывающие больше замыканий, им пришлось бы проверять каждый из них). После того, как вы не нашли a в любом закрытии, он, наконец, просматривает глобальную область и находит ее там.

var a = 1; // global scope
(function myIife(window) {
    var b = 2; // scope in myIife and closure due to reference within innerFunc
    function innerFunc() {
        var c = 3;
        console.log(a);
        console.log(b);
        console.log(c);
    }
    // invoke innerFunc
    innerFunc();
})(window);

Ответ 3

Когда я запускаю фрагмент кода в Chrome, каждый вариант занимает несколько миллисекунд, за исключением прямого доступа к window.x. И неудивительно использовать свойства объекта медленнее, чем использование переменных. Поэтому вопрос только в том, почему window.x медленнее, чем x и даже медленнее, чем что-либо еще.

Это приводит меня к вашей предпосылке, что x = 1; совпадает с window.x = 1;. И мне жаль говорить вам, что это неправильно. FWIW window не является непосредственно глобальным объектом, он как свойством, так и ссылкой на него. Попробуйте window.window.window.window ...

Записи среды

Каждая переменная должна быть "зарегистрирована" в записи среды и существует два основных вида: декларативный и объект.

В области функций используется декларативная запись среды.

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

Он также вид работает наоборот: к каждому свойству этого объекта можно получить доступ через идентификатор с тем же именем. Но это не значит, что вы имеете дело с переменной. Оператор with является еще одним примером использования записи об объектной среде.

Разница между x = 1 и window.x = 1

Создание переменной - это не то же самое, что добавлять свойство к объекту, даже если этот объект является записью среды. Попробуйте Object.getOwnPropertyDescriptor(window, 'x') в обоих случаях. Если x - переменная, то свойство x не configurable. Одним из следствий является то, что вы не можете его удалить.

Когда мы видим только window.x, мы не знаем, является ли это переменной или свойством. Таким образом, без дополнительных знаний мы просто не можем рассматривать его как переменную. Переменные, находящиеся в области действия, в стеке, вы называете его. Компилятор мог проверить, есть ли также переменная x, но эта проверка, вероятно, будет стоить дороже, чем просто делать window.x = window.x + 1. И не забывайте, что window существует только в браузерах. Механизмы JavaScript также работают в других средах, которые могут иметь именованное свойство иначе или даже вообще отсутствуют.

Теперь почему window.x настолько медленнее в Chrome? Интересно, что в Firefox это не так. В моем тестовом прогоне FF намного быстрее, а производительность window.x соответствует параметру любого другого доступа к объекту. То же самое можно сказать и о Safari. Это может быть проблема Chrome. Или доступ к объекту записи среды является медленным в целом, а другие браузеры просто оптимизируются в этом конкретном случае.

Ответ 4

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

Я заметил, что случаи быстрее, когда ссылка на window хранится один раз и далее в цикле, доступ к которому осуществляется через эту ссылку. И случаи, когда window принимает участие в поиске Left Side Side (LHS), каждая итерация в цикле выполняется гораздо медленнее.

Вопрос, почему все случаи имеют разные тайминги, по-прежнему открыт, но, очевидно, это связано с оптимизацией js engine. Один из аргументов для этого - разные браузеры, показывающие разные временные пропорции. Самый странный победитель №3 можно объяснить предположением, что благодаря популярному использованию этот сценарий был хорошо оптимизирован.

Я провел тесты с некоторыми изменениями и получил следующие результаты. Перемещено window.x в window.obj.x и получило одинаковые результаты. Однако, когда x находилось в window.location.x (location также являлось большим нативным объектом), тайминги резко изменились:

1. access x directly    - Completed in 4278ms
2. access window.x     - Completed in 6792ms
3. accessUnqualified() - Completed in 4109ms
4. accessWindowPrefix()- Completed in 6563ms
5. accessCacheWindow() - Completed in 4489ms
6. access IIFE window  - Completed in 4326ms
7. access IIFE x      - Completed in 4137ms