javascript 引擎会优化闭包中定义的常量吗?
Do javascript engines optimize constants defined within closures?
想象一下,我有一个函数可以访问一个常量(从不改变)变量(例如查找 table 或数组)。该常量在函数范围之外的任何地方都没有被引用。
我的直觉告诉我,我应该在函数范围之外定义这个常量(下面的 Option 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 来比较不同的方法:
Object
- 内联(选项 A)
Object
- 常量(选项 B)
建议的其他变体:
Map
- 内联
Map
- 常量
switch
- 内联值
初步发现(在我的机器上,请自行尝试):
- Chrome v77: (4) 是迄今为止最快的,其次是 (2)
- Safari v12.1:(4) 比 (2) 稍快,跨浏览器性能最低
- Firefox v69:(5) 最快,(3) 稍微落后
这里是 V8 开发人员。你的直觉是正确的。
TL;DR: inlinedAccess
每次都创建一个新对象。 constantAccess
更有效,因为它避免了在每次调用时重新创建对象。为了获得更好的性能,请使用 Map
.
"quick test" 为两个函数产生相同时间的事实说明微基准测试很容易产生误导;-)
- 像您示例中的对象那样创建对象的速度非常快,因此影响难以衡量。您可以通过使其更昂贵来放大重复创建对象的影响,例如将一个 属性 替换为
b: new Array(100),
.
'result: ' + ...
中的数字到字符串的转换和随后的字符串连接对总时间的贡献很大;你可以放下它以获得更清晰的信号。
- 对于小型基准测试,您必须小心不要让编译器优化掉所有内容。将结果分配给全局变量就可以了。
- 无论您总是查找相同的 属性 还是不同的属性,都会产生巨大的差异。 JavaScript 中的对象查找并不是一个简单(== 快速)的操作;当 V8 在给定站点总是相同 属性(和相同的对象形状)时,它有一个非常快的 optimization/caching 策略,但是对于不同的属性(或对象形状),它必须进行更昂贵的查找。
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));
我得到以下结果:
inlinedAccess: 1613
constantAccess: 194
mapAccess: 95
integerAccess: 15
switchAccess: 9
综上所述:这些数字是 "milliseconds for 10M lookups"。在实际应用程序中,差异可能太小而不重要,因此您可以编写最多的代码 readable/maintainable/etc。例如,如果您只进行 100K 次查找,则结果为:
inlinedAccess: 31
constantAccess: 6
mapAccess: 6
integerAccess: 5
switchAccess: 4
顺便说一下,这种情况的一个常见变体是 creating/calling 函数。这个:
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即可。)
想象一下,我有一个函数可以访问一个常量(从不改变)变量(例如查找 table 或数组)。该常量在函数范围之外的任何地方都没有被引用。 我的直觉告诉我,我应该在函数范围之外定义这个常量(下面的 Option 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 来比较不同的方法:
Object
- 内联(选项 A)Object
- 常量(选项 B)
Map
- 内联Map
- 常量switch
- 内联值
初步发现(在我的机器上,请自行尝试):
- Chrome v77: (4) 是迄今为止最快的,其次是 (2)
- Safari v12.1:(4) 比 (2) 稍快,跨浏览器性能最低
- Firefox v69:(5) 最快,(3) 稍微落后
这里是 V8 开发人员。你的直觉是正确的。
TL;DR: inlinedAccess
每次都创建一个新对象。 constantAccess
更有效,因为它避免了在每次调用时重新创建对象。为了获得更好的性能,请使用 Map
.
"quick test" 为两个函数产生相同时间的事实说明微基准测试很容易产生误导;-)
- 像您示例中的对象那样创建对象的速度非常快,因此影响难以衡量。您可以通过使其更昂贵来放大重复创建对象的影响,例如将一个 属性 替换为
b: new Array(100),
. 'result: ' + ...
中的数字到字符串的转换和随后的字符串连接对总时间的贡献很大;你可以放下它以获得更清晰的信号。- 对于小型基准测试,您必须小心不要让编译器优化掉所有内容。将结果分配给全局变量就可以了。
- 无论您总是查找相同的 属性 还是不同的属性,都会产生巨大的差异。 JavaScript 中的对象查找并不是一个简单(== 快速)的操作;当 V8 在给定站点总是相同 属性(和相同的对象形状)时,它有一个非常快的 optimization/caching 策略,但是对于不同的属性(或对象形状),它必须进行更昂贵的查找。
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));
我得到以下结果:
inlinedAccess: 1613
constantAccess: 194
mapAccess: 95
integerAccess: 15
switchAccess: 9
综上所述:这些数字是 "milliseconds for 10M lookups"。在实际应用程序中,差异可能太小而不重要,因此您可以编写最多的代码 readable/maintainable/etc。例如,如果您只进行 100K 次查找,则结果为:
inlinedAccess: 31
constantAccess: 6
mapAccess: 6
integerAccess: 5
switchAccess: 4
顺便说一下,这种情况的一个常见变体是 creating/calling 函数。这个:
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即可。)