Мономорфно получить некоторую метаинформацию из произвольного объекта JavaScript (v8)?

Вопрос к знатокам v8.

Недавно я обнаружил ситуацию с полиморфизмом в v8. Дело в том, что полиморфизм хорошо оптимизирован только до 4 "форм" объекта, после чего производительность значительно ухудшается. Классы и наследование объектов игнорируются. Это было очень обескураживающим открытием, потому что с такими ограничениями хорошо структурированный код не будет работать. Кажется, это известная проблема с 2017 года, и маловероятно, что что-то изменится в ближайшем будущем.

Итак, я хочу реализовать лучший полиморфизм в пользовательском пространстве: https://github.com/canonic-epicure/monopoly

Это не новая проблема, она уже решена практически на любом другом языке, с vtables, специализацией кода и т. Д. Любые предложения, как это сделать в JavaScript, очень приветствуются.

Проблема, которую я сейчас пытаюсь решить, - это мономорфное извлечение некоторой метаинформации из произвольного объекта. Эта метаинформация будет аналогом vtable, содержащим информацию для отправки методов. Это сделано, чтобы избежать лишнего поля, содержащего эту информацию.

Метаинформация (некоторый объект, который используется многими другими объектами) в JS естественным образом сопоставляется с прототипом, поэтому 1-й шаг - получить прототип объекта. Это можно сделать мономорфно с помощью 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

2 ответа

полиморфизм хорошо оптимизирован только до 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) или миксинов; быстрое множественное наследование - сложная проблема, особенно в динамических языках.

Говоря о динамических языках: если вы предполагаете, что код может случайным образом видоизменить цепочки прототипов, тогда также будет очень сложно убедиться, что все не разваливается на части (я понятия не имею, как это сделать). Это, кстати, одна из причин, по которой движки JavaScript не могут просто выполнять такое преобразование под капотом: они должны быть на 100% совместимы со спецификациями и должны хорошо работать в самых разных ситуациях. Когда вы создаете свою собственную систему, вы можете наложить определенные ограничения, которые считаете приемлемыми (например: изменение прототипов запрещено), или вы можете выбрать оптимизацию для определенных шаблонов, которые, как вы знаете, важны для вас.

1-й шаг - получить прототип объекта. [...] Это сделано, чтобы избежать лишнего ящика.

Прототип также является "дополнительной коробкой", поэтому здесь нет причин отдавать предпочтение прототипам (и на самом деле, как вы уже заметили, прототипы не достигают того, к чему вы стремитесь).

%OptimizeFunctionOnNextCall(access);

Эта строка в вашем коде не делает ничего полезного. Пожалуйста, не копируйте и вставляйте то, что вы видели в другом месте, не понимая, что они делают. Во-первых, оптимизация не имеет ничего общего с полиморфизмом, поэтому для вашей проблемы это не имеет никакого отношения. Во-вторых, оптимизация в JavaScript имеет смысл только тогда, когда доступна обратная связь по типу из предыдущих, неоптимизированных запусков - если бы оптимизация функций при их первом выполнении была хорошей идеей, движки это сделали бы. Единственная причина, по которой разработчики движков тратят много человеко-лет на написание неоптимизируемых уровней выполнения, заключается в том, что они имеют смысл.

Пытаюсь ответить на свой вопрос, вдохновленный /questions/26588240/konstruktor-dlya-vyizyivaemogo-obekta-v-javascript/26588257#26588257. Еще не совсем уверен, но, похоже, уловка состоит в том, чтобы просто сделать прототип объекта вызываемым.

Так просто как:

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

В отчете о деоптигировании не отображаются мегаморфные встроенные кеши.

Интересно, можно ли также избежать вызова функции и выполнить простой доступ к свойствам.

Другие вопросы по тегам