JavaScript引擎是否优化闭包中定义的常量?

mog*_*rod 6 javascript optimization performance v8

想象一下,我有一个函数可以访问常量(永不突变)(例如,查询表或数组)。在函数范围之外的任何地方都不会引用该常数。我的直觉告诉我,应该在函数范围之外定义此常量(下面的选项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]
}
Run Code Online (Sandbox Code Playgroud)

实际测试

我创建了一个jsperf测试,比较了不同的方法:

  1. Object -内联(选项A)
  2. Object -常数(选项B)

@jmrk建议的其他变体:

  1. Map -内联
  2. Map - 不变
  3. switch -内联值

初步发现(在我的机器上,请自行尝试):

  • Chrome v77:(4)是迄今为止最快的,其次是(2)
  • Safari v12.1:(4)的速度比(2)略快,跨浏览器的性能最低
  • Firefox v69:(5)是最快的,(3)稍落后

jmr*_*mrk 8

V8开发人员在这里。您的直觉是正确的。

TL; DR:inlinedAccess每次创建一个新对象。constantAccess效率更高,因为它避免了每次调用时都重新创建对象。为了获得更好的性能,请使用Map

“快速测试”为两个功能产生相同的计时这一事实说明,微基准测试容易引起误解;-)

  • 创建示例中的对象之类的对象的速度非常快,因此影响很难衡量。您可以通过增加对象创建的成本(例如,用替换一个属性)来扩大重复创建对象的影响b: new Array(100),
  • 数字到字符串的转换以及随后的字符串连接'result: ' + ...对整个时间的贡献很大。您可以将其删除以获得更清晰的信号。
  • 对于小型基准测试,必须注意不要让编译器优化所有内容。将结果分配给全局变量可以解决问题。
  • 无论您始终查找相同的属性还是不同的属性,这也都产生了巨大的差异。JavaScript中的对象查找并非完全简单(==快速)的操作;当V8在给定站点处始终具有相同的属性(和相同的对象形状)时,V8具有非常快速的优化/缓存策略,但是对于变化的属性(或对象形状),V8必须进行更昂贵的查找。
  • Map查找各种键的速度比对象属性查找的速度快。因此,将对象用作地图是2010年,现代JavaScript具有适当Map的,所以请使用它们!:-)
  • Array 元素查找甚至更快,但是当然只有键为整数时才可以使用它们。
  • 当查找的可能键的数量很少时,switch语句很难被击败。但是,它们不能很好地扩展到大量键。

让我们将所有这些想法放入代码中:

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));
Run Code Online (Sandbox Code Playgroud)

我得到以下结果:

inlinedAccess: 1613
constantAccess: 194
mapAccess: 95
integerAccess: 15
switchAccess: 9
Run Code Online (Sandbox Code Playgroud)

所有这些都说明了这些数字:“毫秒数为1000万次查找”。在实际的应用程序中,差异可能太小而无关紧要,因此您可以编写最易读/可维护的代码。例如,如果仅执行100K查找,则结果为:

inlinedAccess: 31
constantAccess: 6
mapAccess: 6
integerAccess: 5
switchAccess: 4
Run Code Online (Sandbox Code Playgroud)

顺便说一句,这种情况的常见变体是创建/调用函数。这个:

function singleton_callback(...) { ... }
function efficient(...) {
  return singleton_callback(...);
}
Run Code Online (Sandbox Code Playgroud)

比这更有效:

function wasteful(...) {
  function new_callback_every_time(...) { ... }
  return new_callback_every_time(...);
}
Run Code Online (Sandbox Code Playgroud)

同样,这是:

function singleton_method(args) { ... }
function EfficientObjectConstructor(param) {
  this.___ = param;
  this.method = singleton_method;
}
Run Code Online (Sandbox Code Playgroud)

比这更有效:

function WastefulObjectConstructor(param) {
  this.___ = param;
  this.method = function(...) { 
    // Allocates a new function every time.
  };
}
Run Code Online (Sandbox Code Playgroud)

(当然,通常的做法是Constructor.prototype.method = function(...) {...},它还可以避免重复创建函数。或者,如今,您可以只使用classes。)