Перейти к основному содержимому

Супербыстрый доступ к свойствам `super`

· 6 мин. чтения
[Марья Хётта](https://twitter.com/marjakh), супероптимизатор

super может быть использован для доступа к свойствам и методам родителя объекта.

Ранее доступ к свойству super (например, super.x) выполнялся через вызов времени выполнения. Начиная с V8 v9.0, мы используем систему кеширования inline (IC) в неоптимизированном коде и генерируем правильный оптимизированный код для доступа к свойствам super, без необходимости перемещаться к выполнению времени.

Как вы можете видеть из графиков ниже, доступ к свойствам super раньше был на порядок медленнее, чем обычный доступ к свойствам из-за вызова времени выполнения. Теперь разница значительно уменьшилась.

Сравнение доступа к свойствам super с обычным доступом, оптимизировано

Сравнение доступа к свойствам super с обычным доступом, не оптимизировано

Доступ к свойствам super сложно оценить, так как он должен происходить внутри функции. Мы не можем измерить отдельные запросы свойств, только крупные блоки работы. Таким образом, накладные расходы на вызов функции включены в измерения. Графики выше несколько недооценивают разницу между доступом к свойствам super и обычным доступом, но они достаточно точны, чтобы продемонстрировать разницу между старым и новым доступом к свойствам super.

В неоптимизированном режиме (режим интерпретации) доступ к свойствам super всегда будет медленнее, чем обычный доступ к свойствам, так как требуется больше операций (чтение home object из контекста и чтение __proto__ из home object). В оптимизированном коде мы уже встраиваем home object как константу, если это возможно. Это можно улучшить, встраивая также его __proto__ как константу.

Прототипное наследование и super

Начнем с основ — что вообще означает доступ к свойствам super?

class A { }
A.prototype.x = 100;

class B extends A {
m() {
return super.x;
}
}
const b = new B();
b.m();

Теперь A является суперклассом для B, а b.m() возвращает 100, как и ожидалось.

Диаграмма наследования классов

Реальность прототипного наследования в JavaScript гораздо сложнее:

Диаграмма прототипного наследования

Нужно четко различать свойства __proto__ и prototype — они не означают одно и то же! Чтобы сделать это еще более запутанным, объект b.__proto__ часто называют «прототипом объекта b».

b.__proto__ — это объект, от которого b наследует свойства. B.prototype — это объект, который станет __proto__ для объектов, созданных с помощью new B(), то есть b.__proto__ === B.prototype.

В свою очередь, у B.prototype есть собственное свойство __proto__, равное A.prototype. Вместе это формирует цепочку прототипов:

b ->
b.__proto__ === B.prototype ->
B.prototype.__proto__ === A.prototype ->
A.prototype.__proto__ === Object.prototype ->
Object.prototype.__proto__ === null

Через эту цепочку b может получить доступ ко всем свойствам, определенным в любом из этих объектов. Метод m является свойством B.prototypeB.prototype.m, — и поэтому b.m() работает.

Теперь мы можем определить super.x внутри m как поиск свойства, где мы начинаем искать свойство x в __proto__ home object и движемся вверх по цепочке прототипов, пока не найдем его.

Home object — это объект, в котором определен метод. В данном случае home object для m — это B.prototype. Его __proto__ — это A.prototype, так что именно там мы начинаем искать свойство x. Мы назовем A.prototype объектом начала поиска. В данном случае мы находим свойство x сразу в объекте начала поиска, но в общем случае оно может также находиться выше в цепочке прототипов.

Если у B.prototype есть свойство x, мы его игнорируем, так как начинаем искать выше него в цепочке прототипов. Кроме того, в данном случае поиск свойства super не зависит от получателя — объекта, который является значением this при вызове метода.

B.prototype.m.call(some_other_object); // все равно возвращает 100

Если у свойства есть геттер, получатель будет передан в геттер в качестве значения this.

В резюме: в доступе к свойствам super, super.x, объектом начала поиска является __proto__ home object, а получатель — это получатель метода, в котором происходит доступ к свойствам super.

При обычном доступе к свойству, o.x, мы начинаем поиск свойства x в объекте o и перемещаемся вверх по цепочке прототипов. Мы также используем o в качестве получателя, если у x есть геттер — объект поиска и получатель являются одним и тем же объектом (o).

Доступ к свойству через super похож на обычный доступ к свойству, но объект поиска и получатель — разные.

Реализация ускоренного super

Указанный выше вывод также является ключом к реализации быстрого доступа к свойствам через super. V8 уже разработан для обеспечения быстрого доступа к свойствам — теперь мы обобщили его для случая, когда объект поиска и получатель различаются.

Система кэширования V8 с использованием встроенных данных является основной частью для реализации быстрого доступа к свойствам. Вы можете прочитать о ней в высокоуровневом введении, указанном выше, или более подробных описаниях представления объектов V8 и реализации системы кэширования V8 с использованием встроенных данных.

Для ускорения работы с super мы добавили новый байткод Ignition, LdaNamedPropertyFromSuper, который позволяет подключиться к системе IC в интерпретируемом режиме, а также создавать оптимизированный код для доступа к свойствам через super.

С помощью нового байткода мы можем добавить новый IC, LoadSuperIC, для ускорения загрузки свойств через super. Аналогично LoadIC, который обрабатывает обычную загрузку свойств, LoadSuperIC отслеживает формы объектов поиска, которые он видел, и запоминает, как загружать свойства из объектов, имеющих одну из этих форм.

LoadSuperIC повторно использует существующий механизм IC для загрузки свойств, просто с другим объектом поиска. Так как слой IC уже различает объект поиска и получателя, реализация должна была быть легкой. Но так как объект поиска и получатель всегда были одинаковыми, возникали ошибки, когда мы использовали объект поиска, хотя имелся в виду получатель, и наоборот. Эти ошибки были исправлены, и теперь мы правильно поддерживаем случаи, когда объект поиска и получатель различаются.

Оптимизированный код для доступа к свойствам через super создается на этапе JSNativeContextSpecialization компилятора TurboFan. Реализация обобщает существующий механизм поиска свойств (JSNativeContextSpecialization::ReduceNamedAccess) для обработки случая, когда получатель и объект поиска различаются.

Оптимизированный код стал еще более эффективным, когда мы переместили домашний объект из JSFunction, где он хранился. Теперь он хранится в контексте класса, что позволяет TurboFan внедрить его в оптимизированный код как константу, когда это возможно.

Другие использования super

super внутри методов литералов объектов работает так же, как внутри методов классов, и оптимизируется аналогично.

const myproto = {
__proto__: { 'x': 100 },
m() { return super.x; }
};
const o = { __proto__: myproto };
o.m(); // возвращает 100

Конечно, существуют ограниченные случаи, которые мы не оптимизировали. Например, запись свойств через super (super.x = ...) не оптимизирована. Кроме того, использование миксинов делает место доступа мегаморфным, что приводит к более медленному доступу к свойствам через super:

function createMixin(base) {
class Mixin extends base {
m() { return super.m() + 1; }
// ^ это место доступа является мегаморфным
}
return Mixin;
}

class Base {
m() { return 0; }
}

const myClass = createMixin(
createMixin(
createMixin(
createMixin(
createMixin(Base)
)
)
)
);
(new myClass()).m();

Потребуется еще работа, чтобы обеспечить максимально быструю работу всех объектно-ориентированных паттернов — следите за дальнейшими оптимизациями!