JS中如何使用原型方法添加新类以实现V8优化?

时间:2018-12-26 19:41:20

标签: javascript node.js v8 winston

我正在阅读Winston的代码库,他们的DerivedLogger class line 22中有一条评论说:

  

创建一个新的类派生记录器,可以将其级别附加到其原型。这是V8优化,众所周知可以提高原型功能的性能。

根据我在这里收集的信息,他们说添加新类(DerivedLogger)和原型方法是V8优化的一种众所周知的形式吗?与仅将方法添加到类Logger的原型而不必创建新类有什么不同?如果我对这里的评论有误解,有人可以帮助我理解这个概念或纠正我吗?谢谢!

1 个答案:

答案 0 :(得分:2)

嗯,这很有趣。

前言

没有关于“众所周知的”“提高性能”的任何细节,我们只能推测是什么意思。

记录器的历史记录

当我第一次看到您的问题和代码时,我意识到代码注释必须过时了。

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)
);

Source

这是错误的,因为通过构造函数中的_setupLevels()调用,方法是在实例上定义的,而不是在原型上定义的。有关该主题的详细信息,请参见herehere

因此,我翻查了历史,以发现未更改代码的注释的首次出现...

这是添加上述注释时的原始代码:

// 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) {

Source

当前代码以另一种方式更改:DerivedLogger不再与每个记录器实例一起创建,而是在模块加载时仅创建一次。

分析

直到这里,我还没有意识到Winston作者在记录器的create函数中创建了新的原型:

// Create a new instance of a winston Logger. Creates a new
// prototype for each instance.
//
module.exports = function (opts) {

Source

因此,当要创建一个新的记录器时,不仅要创建一个实例,而且还要创建一个全新的原型。

               [Logger]  (A)
                   ^
                   |
         +---------+--------+
         |                  |
 [DerivedLogger #1] [DerivedLogger #2]  (B)
         |                  |
      logger #1          logger #2

派生的记录器不会重复使用。

结论

最初的意图是在创建新的记录器实例时阻止Logger(A)的修改/污染。

尽管在原型上创建记录器方法以防止在重复的实例方法中浪费内存(请参见开头的链接问题),似乎受到重复创建新原型的阻碍。

我什至认为,创建原型持有日志方法而不是直接为实例定义日志方法所获得的性能会因原型对象的创建而被吞并。

但是,我不是100%相信所讨论的解释是原始意图,并且愿意进行更正和澄清。

奖金

(我在研究过程中发现了这一点,可能与上述Winston代码无关。)

由于令原作者声称在原型上定义方法会优化V8的事情困扰我,我开始寻找有关此主题的更新,然后发现V8开发人员Mathias Bynens的文章:JavaScript engine fundamentals: optimizing prototypes

他正在讨论大多数Javascript引擎(不仅是V8!)如何在内部存储对象以及它们如何处理属性访问。您可能还想阅读另一个article by him on Shape objects

尽管在V8上似乎有一个独特的细节,但我不会在这里完全详细介绍:如何处理原型链中的访问:

  

V8为此专门处理原型形状。每个原型都有一个唯一的形状,该形状不会与其他任何对象共享(特别是与其他原型不共享),并且每个原型形状都有一个与之关联的特殊ValidityCell
  只要有人更改关联的原型或其上方的任何原型,此ValidityCell便会失效。
  [...]
  下次单击内联缓存时,引擎必须检查实例和ValidityCell的形状。如果仍然有效,则引擎可以直接访问“原型”上的“偏移”,而无需进行其他查找。

(我的粗体字。)

因此,对于V8而言,似乎唯一的事实是,它们跟踪原型是否仍然处于“成形状态”。这使V8减少了原型链处理中涉及的检查。