Оптимизируют ли движки JavaScript константы, определенные в замыканиях? - PullRequest
6 голосов
/ 17 октября 2019

Представьте, что у меня есть функция, которая обращается к постоянной (никогда не изменяемой) переменной (например, к таблице или массиву поиска). На константу не ссылаются нигде за пределами области действия функции. Моя интуиция подсказывает мне, что я должен определить эту константу вне области действия функции ( Опция A ниже), чтобы избежать (повторного) ее создания при каждом вызове функции, но действительно ли так работают современные движки Javascript? Я хотел бы думать, что современные движки могут видеть, что константа никогда не изменяется, и, следовательно, должна только создать и кэшировать ее один раз ( есть ли термин для этого? ). Кешируют ли браузеры функции, определенные в замыканиях, таким же образом?

Существуют ли какие-либо незначительные потери производительности, связанные с простым определением константы внутри функции, рядом с местом, где она вызывается ( Опция B )? Отличается ли ситуация для более сложных объектов?

// Option A:
function inlinedAccess(key) {
  const inlinedLookupTable = {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
  }

  return 'result: ' + inlinedLookupTable[key]
}

// Option B:
const CONSTANT_TABLE = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
}
function constantAccess(key) {
  return 'result: ' + CONSTANT_TABLE[key]
}

Тестирование на практике

Я создал jsperf test , который сравнивает различные подходы:

  1. Object - встроенный (опция A)
  2. Object - постоянный (опция B)

Дополнительные варианты, предложенные @ jmrk

Map - встроенный Map - постоянный switch - встроенный значения

Исходные данные (на моей машине,не стесняйтесь попробовать сами):

  • Chrome v77: (4), безусловно, самый быстрый, за которым следует (2)
  • Safari v12.1:(4) немного быстрее, чем (2), самая низкая производительность в браузерах
  • Firefox v69: (5) самая быстрая, с (3) немного позади

1 Ответ

8 голосов
/ 17 октября 2019

V8 разработчик здесь. Ваша интуиция верна.

TL; DR: inlinedAccess каждый раз создает новый объект. constantAccess более эффективен, поскольку позволяет избежать воссоздания объекта при каждом вызове. Для еще лучшей производительности используйте Map.

Тот факт, что «быстрый тест» выдает одинаковые значения времени для обеих функций, показывает, насколько легко микробенчмарки могут вводить в заблуждение; -)

  • Создание объектов, подобных объекту в вашем примере, выполняется довольно быстро, поэтому влияние трудно измерить. Вы можете усилить влияние повторного создания объекта, сделав его более дорогим, например, заменив одно свойство на b: new Array(100),.
  • Преобразование числа в строку и последующее объединение строк в 'result: ' + ... вносят значительный вклад вобщее время;Вы можете пропустить это, чтобы получить более четкий сигнал.
  • Для небольшого теста вы должны быть осторожны, чтобы компилятор не оптимизировал все. Назначение результата глобальной переменной делает свое дело.
  • Это также имеет огромное значение, независимо от того, ищите ли вы всегда одно и то же свойство или разные свойства. Поиск объектов в JavaScript не совсем простая (== быстрая) операция;V8 имеет очень быструю стратегию оптимизации / кэширования, когда у данного сайта всегда одно и то же свойство (и одна и та же форма объекта), но для изменяющихся свойств (или формы объекта) он должен выполнять более дорогой поиск.
  • Map поиск различных ключей выполняется быстрее, чем поиск свойств объекта. Использование объектов в качестве карт - это 2010 год, современный JavaScript имеет правильные значения Map s, поэтому используйте их! : -)
  • Array поиск элементов еще быстрее, но, конечно, вы можете использовать их только тогда, когда ваши ключи целые.
  • Когда число возможных ключей, которые ищут, малозаявления переключателя трудно превзойти. Однако они плохо масштабируются для большого количества ключей.

Давайте вложим все эти мысли в код:

function inlinedAccess(key) {
  const inlinedLookupTable = {
    a: 1,
    b: new Array(100),
    c: 3,
    d: 4,
  }
  return inlinedLookupTable[key];
}

const CONSTANT_TABLE = {
  a: 1,
  b: new Array(100),
  c: 3,
  d: 4,
}
function constantAccess(key) {
  return CONSTANT_TABLE[key];
}

const LOOKUP_MAP = new Map([
  ["a", 1],
  ["b", new Array(100)],
  ["c", 3],
  ["d", 4]
]);
function mapAccess(key) {
  return LOOKUP_MAP.get(key);
}

const ARRAY_TABLE = ["a", "b", "c", "d"]
function integerAccess(key) {
  return ARRAY_TABLE[key];
}

function switchAccess(key) {
  switch (key) {
    case "a": return 1;
    case "b": return new Array(100);
    case "c": return 3;
    case "d": return 4;
  }
}

const kCount = 10000000;
let result = null;
let t1 = Date.now();
for (let i = 0; i < kCount; i++) {
  result = inlinedAccess("a");
  result = inlinedAccess("d");
}
let t2 = Date.now();
for (let i = 0; i < kCount; i++) {
  result = constantAccess("a");
  result = constantAccess("d");
}
let t3 = Date.now();
for (let i = 0; i < kCount; i++) {
  result = mapAccess("a");
  result = mapAccess("d");
}
let t4 = Date.now();
for (let i = 0; i < kCount; i++) {
  result = integerAccess(0);
  result = integerAccess(3);
}
let t5 = Date.now();
for (let i = 0; i < kCount; i++) {
  result = switchAccess("a");
  result = switchAccess("d");
}
let t6 = Date.now();
console.log("inlinedAccess: " + (t2 - t1));
console.log("constantAccess: " + (t3 - t2));
console.log("mapAccess: " + (t4 - t3));
console.log("integerAccess: " + (t5 - t4));
console.log("switchAccess: " + (t6 - t5));

Я получаю следующие результаты:

inlinedAccess: 1613
constantAccess: 194
mapAccess: 95
integerAccess: 15
switchAccess: 9

Все, что сказал: эти числа "миллисекунды для 10M поисков". В реальных приложениях различия, вероятно, слишком малы, чтобы иметь значение, поэтому вы можете написать любой код, который будет наиболее читаемым / поддерживаемым / и т.д. Например, если вы выполняете только 100К-поиски, результаты будут такими:

inlinedAccess: 31
constantAccess: 6
mapAccess: 6
integerAccess: 5
switchAccess: 4

Кстати, распространенным вариантом этой ситуации является создание / вызов функций. Это:

function singleton_callback(...) { ... }
function efficient(...) {
  return singleton_callback(...);
}

гораздо эффективнее, чем это:

function wasteful(...) {
  function new_callback_every_time(...) { ... }
  return new_callback_every_time(...);
}

И, аналогично, это:

function singleton_method(args) { ... }
function EfficientObjectConstructor(param) {
  this.___ = param;
  this.method = singleton_method;
}

гораздо эффективнее, чем это:

function WastefulObjectConstructor(param) {
  this.___ = param;
  this.method = function(...) { 
    // Allocates a new function every time.
  };
}

(Конечно, обычный способ сделать это - Constructor.prototype.method = function(...) {...}, что также позволяет избежать повторного создания функций. В настоящее время вы можете просто использовать class es.)

...