添加带有原型方法的新 class 如何成为 JS 中 V8 优化的一种形式?
How is adding a new class with prototype methods a form of V8 optimization in JS?
我正在阅读 Winston 的代码库,他们的 DerivedLogger class line 22 中有一条评论说:
Create a new class derived logger for which the levels can be attached to the prototype of. This is a V8 optimization that is well known to increase performance of prototype functions.
根据我在这里收集到的信息,他们说添加一个新的 class (DerivedLogger) 来添加原型方法是一种众所周知的 V8 优化形式?它与仅将方法添加到 class Logger
的原型而无需创建新的 class 有何不同?如果我误解了这里的评论,有人可以帮助我理解这个概念或纠正我吗?谢谢!
嗯,这很有趣。
前言
没有任何详细信息 "well known" "increase [on] performance" 我们只能推测其含义。
记录器的历史
当我第一次看到你的问题和代码时,我意识到代码注释一定是过时的。
class DerivedLogger extends Logger {
/**
* Create a new class derived logger for which the levels can be attached to
* the prototype of. This is a V8 optimization that is well know to increase
* performance of prototype functions.
* @param {!Object} options - Options for the created logger.
*/
constructor(options) {
super(options);
this._setupLevels();
}
// ...
}
module.exports = (opts = { levels: config.npm.levels }) => (
new DerivedLogger(opts)
);
它在一个方面是错误的,因为通过构造函数中的 _setupLevels()
调用,方法是在实例上定义的,而不是在原型上。有关该主题的详细信息,请参阅 here or here。
所以我翻遍了历史,找到了第一次出现代码不变的评论...
这是添加上述注释后原始代码的样子:
// Create a new instance of a winston Logger. Creates a new
// prototype for each instance.
module.exports = function (opts) {
// ...
//
// Create a new prototypal derived logger for which the levels
// can be attached to the prototype of. This is a V8 optimization
// that is well know to increase performance of prototype functions.
//
function DerivedLogger(options) { Logger.call(this, options); }
util.inherits(DerivedLogger, Logger);
// ...
DerivedLogger.prototype[level] = function (msg) {
当前代码以另一种方式更改:DerivedLogger
不再为每个记录器实例创建,而是仅在加载模块时创建一次。
分析
直到这里我才意识到 Winston 作者在记录器的创建函数中创建了新的原型:
// Create a new instance of a winston Logger. Creates a new
// prototype for each instance.
//
module.exports = function (opts) {
因此,当要创建一个新的记录器时,不仅会创建一个实例,还会创建一个全新的原型。
[Logger] (A)
^
|
+---------+--------+
| |
[DerivedLogger #1] [DerivedLogger #2] (B)
| |
logger #1 logger #2
不重复使用派生的记录器。
结论
最初的意图明确是为了在创建新记录器实例时阻止 modification/pollution of Logger
(A)。
尽管在原型上创建记录器方法以防止在重复实例方法中浪费内存(请参阅开头的链接问题)似乎因重复创建新原型而受阻。
我什至相信通过创建一个原型保存日志方法比直接为实例定义它们所获得的性能会被原型对象的创建所吞没。
但是,我不能 100% 确信所讨论的解释是原意,并且愿意接受更正和澄清。
奖金
(这是我在研究过程中发现的,可能与上述 Winston 代码无关。)
因为原作者声称在原型上定义方法会优化 V8 的东西让我很困扰,所以我开始搜索关于这个主题的更新,我找到了一篇 V8 开发人员 Mathias Bynens 的文章:JavaScript engine fundamentals: optimizing prototypes。
他正在讨论大多数 Javascript 引擎(不仅是 V8!)如何在内部存储对象以及它们如何处理 属性 访问。您可能还想阅读另一个 article by him on Shape objects.
我不会在这里详细回顾,尽管似乎有一个 V8 的独特细节如何处理沿原型链的访问:
V8 treats prototype shapes specially for this purpose. Each prototype has a unique shape that is not shared with any other objects (specifically not with other prototypes), and each of these prototype shapes has a special ValidityCell
associated with it.
This ValidityCell
is invalidated whenever someone changes the associated prototype or any prototype above it.
[...]
The next time the Inline Cache is hit, the engine has to check the shape of the instance and the ValidityCell
. If it’s still valid, the engine can reach out directly to the Offset on the Prototype, skipping the additional lookups.
(我加粗的文字。)
因此,V8 的独特之处似乎在于,他们会跟踪 原型 是否仍然是 "in shape"。这允许 V8 减少原型链处理中涉及的检查。
我正在阅读 Winston 的代码库,他们的 DerivedLogger class line 22 中有一条评论说:
Create a new class derived logger for which the levels can be attached to the prototype of. This is a V8 optimization that is well known to increase performance of prototype functions.
根据我在这里收集到的信息,他们说添加一个新的 class (DerivedLogger) 来添加原型方法是一种众所周知的 V8 优化形式?它与仅将方法添加到 class Logger
的原型而无需创建新的 class 有何不同?如果我误解了这里的评论,有人可以帮助我理解这个概念或纠正我吗?谢谢!
嗯,这很有趣。
前言
没有任何详细信息 "well known" "increase [on] performance" 我们只能推测其含义。
记录器的历史
当我第一次看到你的问题和代码时,我意识到代码注释一定是过时的。
class DerivedLogger extends Logger {
/**
* Create a new class derived logger for which the levels can be attached to
* the prototype of. This is a V8 optimization that is well know to increase
* performance of prototype functions.
* @param {!Object} options - Options for the created logger.
*/
constructor(options) {
super(options);
this._setupLevels();
}
// ...
}
module.exports = (opts = { levels: config.npm.levels }) => (
new DerivedLogger(opts)
);
它在一个方面是错误的,因为通过构造函数中的 _setupLevels()
调用,方法是在实例上定义的,而不是在原型上。有关该主题的详细信息,请参阅 here or here。
所以我翻遍了历史,找到了第一次出现代码不变的评论...
这是添加上述注释后原始代码的样子:
// Create a new instance of a winston Logger. Creates a new
// prototype for each instance.
module.exports = function (opts) {
// ...
//
// Create a new prototypal derived logger for which the levels
// can be attached to the prototype of. This is a V8 optimization
// that is well know to increase performance of prototype functions.
//
function DerivedLogger(options) { Logger.call(this, options); }
util.inherits(DerivedLogger, Logger);
// ...
DerivedLogger.prototype[level] = function (msg) {
当前代码以另一种方式更改:DerivedLogger
不再为每个记录器实例创建,而是仅在加载模块时创建一次。
分析
直到这里我才意识到 Winston 作者在记录器的创建函数中创建了新的原型:
// Create a new instance of a winston Logger. Creates a new
// prototype for each instance.
//
module.exports = function (opts) {
因此,当要创建一个新的记录器时,不仅会创建一个实例,还会创建一个全新的原型。
[Logger] (A)
^
|
+---------+--------+
| |
[DerivedLogger #1] [DerivedLogger #2] (B)
| |
logger #1 logger #2
不重复使用派生的记录器。
结论
最初的意图明确是为了在创建新记录器实例时阻止 modification/pollution of Logger
(A)。
尽管在原型上创建记录器方法以防止在重复实例方法中浪费内存(请参阅开头的链接问题)似乎因重复创建新原型而受阻。
我什至相信通过创建一个原型保存日志方法比直接为实例定义它们所获得的性能会被原型对象的创建所吞没。
但是,我不能 100% 确信所讨论的解释是原意,并且愿意接受更正和澄清。
奖金
(这是我在研究过程中发现的,可能与上述 Winston 代码无关。)
因为原作者声称在原型上定义方法会优化 V8 的东西让我很困扰,所以我开始搜索关于这个主题的更新,我找到了一篇 V8 开发人员 Mathias Bynens 的文章:JavaScript engine fundamentals: optimizing prototypes。
他正在讨论大多数 Javascript 引擎(不仅是 V8!)如何在内部存储对象以及它们如何处理 属性 访问。您可能还想阅读另一个 article by him on Shape objects.
我不会在这里详细回顾,尽管似乎有一个 V8 的独特细节如何处理沿原型链的访问:
V8 treats prototype shapes specially for this purpose. Each prototype has a unique shape that is not shared with any other objects (specifically not with other prototypes), and each of these prototype shapes has a special
ValidityCell
associated with it.
ThisValidityCell
is invalidated whenever someone changes the associated prototype or any prototype above it.
[...]
The next time the Inline Cache is hit, the engine has to check the shape of the instance and theValidityCell
. If it’s still valid, the engine can reach out directly to the Offset on the Prototype, skipping the additional lookups.
(我加粗的文字。)
因此,V8 的独特之处似乎在于,他们会跟踪 原型 是否仍然是 "in shape"。这允许 V8 减少原型链处理中涉及的检查。