2

v8 专家的问题。

最近,我在 v8 中发现了多态的情况。正是多态性仅对 4 个对象“形状”进行了很好的优化,之后性能显着下降。类和对象继承被忽略。这是一个非常令人沮丧的发现,因为在这些限制下,结构良好的代码将无法执行。自 2017 年以来,这似乎是一个众所周知的问题,而且在不久的将来不太可能发生任何变化。

所以,我愿意在用户空间实现更好的多态性:https ://github.com/canonic-epicure/monopoly

这不是一个新问题,它已经用几乎任何其他语言解决了,包括 vtables、代码专业化等。非常欢迎任何关于如何在 JavaScript 中完成的建议。

我目前试图解决的问题是从任意对象单态检索一些元信息。此元信息将是一个 vtable 模拟,包含方法分派的信息。这是为了避免包含此信息的额外框。

JS 中的元信息(很多其他对象共享的某个对象)自然映射到原型,所以第一步就是获取对象的原型。这可以用 单态完成Object.getPrototypeOf()。但是,似乎无论你尝试什么,你都会失去单态性。

例如,在以下代码中,对对象的构造函数的访问将是超态的:

class HasVTable {}

HasVTable.prototype.vtable = {}

class Class1 extends HasVTable {}
class Class2 extends HasVTable {}
class Class3 extends HasVTable {}
class Class4 extends HasVTable {}
class Class5 extends HasVTable {}

function access(obj) {
    console.log(Object.getPrototypeOf(obj).constructor.name);
}

%OptimizeFunctionOnNextCall(access);

access(new Class1);
access(new Class2);
access(new Class3);
access(new Class4);
access(new Class5);

所以问题是,如何在原型中存储一些信息然后检索它,而不会失去单态性?也许,一些“知名”符号可以在这里提供帮助?还是有其他解决方案?

谢谢!


例如,我刚刚尝试使用迭代器符号,但没有成功 -proto在迭代器位置访问仍然是超态的:

class HasVTable {}

class Class1 extends HasVTable {
    *[Symbol.iterator] () {
        yield 'Class1'
    }
}
class Class2 extends HasVTable {
    *[Symbol.iterator] () {
        yield 'Class2'
    }
}
class Class3 extends HasVTable {
    *[Symbol.iterator] () {
        yield 'Class3'
    }
}
class Class4 extends HasVTable {
    *[Symbol.iterator] () {
        yield 'Class4'
    }
}
class Class5 extends HasVTable {
    *[Symbol.iterator] () {
        yield 'Class5'
    }
}

function access(obj) {
    const proto = Object.getPrototypeOf(obj)

    let res

    for (res of proto) break

    console.log(res)
}

%OptimizeFunctionOnNextCall(access);

access(new Class1);
access(new Class2);
access(new Class3);
access(new Class4);
access(new Class5);

更新 2020/10/21

我使用了一个出色的deoptigate工具来跟踪代码反优化:

npx deoptigate --allow-natives-syntax -r esm src_js/draft3.js

4

2 回答 2

3

多态性仅针对 4 个对象“形状”进行了很好的优化,之后性能显着下降。

不完全的。“多态”方法对于少量形状来说很快,但不能很好地扩展。“超变形”方法更适用于在同一地点看到的大量形状。阈值应该是 3、4(目前是)、5、6 还是其他值,很大程度上取决于您正在查看的特定基准;我见过限制为 8 的情况会更好,但在其他情况下,当前限制为 4 更好。更高的值也会消耗更多的内存,这对于小型独立测试无关紧要,但对于大型应用程序来说是一个重要的考虑因素。

如何在原型中存储一些信息然后检索它,而不会失去单态性?

如果您想要单态访问,请坚持使用一种对象形状。这就是它的全部。您只需要弄清楚如何将该原则应用于您的情况。您最终可能会使用对象对来表示每个高级对象:一部分用于单态、与类型无关的共享部分,一部分(从第一个链接)用于特定于每个类的事物。大概是:

// Generic part of the object pair:
class HasVTable {
  constructor(vtable, name, object_data) {
    this.vtable = vtable;
    // This is just an example of the simplest possible way to implement
    // properties that are guaranteed to be present in all objects.
    // Totally optional; `vtable` and `object_data` are the ideas that matter.
    this.name = name;
    this.object_data = object_data;
  }

  // Note that `doStuff` might not exist on all "subclasses". Just don't
  // attempt to call it where it would be invalid. Or add a check, if you
  // want to pay a (small) performance cost for better robustness.
  doStuff(...) {
    // The fact that "doStuff" is the first method is hardcoded here and
    // elsewhere. That hardcoding is what makes vtables fast.
    return this.vtable[0](this.object_data, ...);

    // Alternatively, you could get fancy and use:
    return this.vtable[0].call(this.object_data, ...);
    // And then `Class1_doStuff` below could use `this` like normal.
    // I'm not sure which is faster, you'd have to measure.
  }
  getName() {
    // Fields that are guaranteed to be present on all objects can be
    // stored and retrieved directly:
    return this.name;
  }
}

// The following is the class-specific part of the object pair.
// I'm giving only Class1 here for brevity.
// This does *not* derive from anything, and is never constructed directly.
class Class1 {
  constructor(...) {
    this.class1_specific_field = "hello world";
  }
}

function Class1_doStuff(this_class1) {
  // Use `this_class1` instead of `this` in here.
  console.log(this_class1.class1_specific_field);
}

// Note that only one instance of this exists, it's shared by all class1 objects.
let Class1_vtable = [
  Class1_doStuff,  // doStuff is hardcoded to be the first method.
]

// Replaces `new Class1(...)` in traditional class-based code.
// Could also make this a static function: `Class1.New = function(...) {...}`.
function NewClass1(...) {
  return new HasVTable(Class1_vtable, "Class1", new Class1(...));
}

这种技术可以显着提高速度,至少对于相当简单的对象层次结构来说是这样。因此,对此进行试验可能会给您带来有益的结果。祝你好运!

对于如何实现多重继承(正如你的 github 项目描述所说的你想做的那样)或 mixins,我没有任何建议;快速多重继承是一个难题,尤其是在动态语言中。

说到动态语言:如果您假设代码可能会随机改变原型链,那么确保一切不会崩溃也将非常困难(我不知道您如何实现这一点)。顺便说一句,这就是 JavaScript 引擎不能只在底层执行这种转换的原因之一:它们必须 100% 符合规范,并且必须在各种情况下都能很好地工作。当您构建自己的系统时,您可以选择施加某些您认为可以接受的限制(例如:禁止修改原型),或者您可以选择针对您知道对您很重要的特定模式进行优化。

第一步是获取对象的原型。[...] 这是为了避免额外的盒子。

原型也是一个“额外的盒子”,所以这里没有理由更喜欢原型(事实上,正如您已经注意到的那样,原型并不能实现您的目标)。

%OptimizeFunctionOnNextCall(access);

代码中的那一行完全没有任何用处。请不要只是复制粘贴您在其他地方看到的东西而不了解它们的作用。首先,优化与多态性无关,因此与您的问题完全无关。其次,JavaScript 中的优化只有在以前的非优化运行的类型反馈可用时才有意义——如果在第一次执行时优化函数是个好主意,引擎会这样做。引擎开发人员花费很多人年的精力来编写非优化执行层的唯一原因是拥有它们是有意义的。

于 2020-10-19T15:33:26.857 回答
0

受https://stackoverflow.com/a/17111430/365104启发,试图回答我自己的问题。还不太确定,但似乎诀窍就是让对象的原型 callable

很简单:

function HasVTable(arg) {
    return function () { return arg }
}

const Class1 = function () {}
Class1.prototype = HasVTable('class1_vtable')
Class1.prototype.some1 = function () {}

const Class2 = function () {}
Class2.prototype = HasVTable('class2_vtable')
Class2.prototype.some2 = function () {}

const Class3 = function () {}
Class3.prototype = HasVTable('class3_vtable')
Class3.prototype.some3 = function () {}

const Class4 = function () {}
Class4.prototype = HasVTable('class4_vtable')
Class4.prototype.some4 = function () {}

const Class5 = function () {}
Class5.prototype = HasVTable('class5_vtable')
Class5.prototype.some5 = function () {}


function access(obj) {
    console.log(Object.getPrototypeOf(obj)());
}

%OptimizeFunctionOnNextCall(access);

%OptimizeFunctionOnNextCall(Class1);
%OptimizeFunctionOnNextCall(Class2);
%OptimizeFunctionOnNextCall(Class3);
%OptimizeFunctionOnNextCall(Class4);
%OptimizeFunctionOnNextCall(Class5);

access(new Class1);
access(new Class2);
access(new Class3);
access(new Class4);
access(new Class5);

Deoptigate 报告不显示任何超大型内联缓存。

我想知道是否也可以避免函数调用并执行一些简单的属性访问。

于 2020-10-19T19:41:35.683 回答