メインコンテンツまでスキップ

超高速な`super`プロパティのアクセス

· 約8分
[Marja Hölttä](https://twitter.com/marjakh), super optimizer

superキーワードは、オブジェクトの親に存在するプロパティや関数にアクセスするために使用できます。

以前は、superプロパティ(例えばsuper.x)へのアクセスはランタイム呼び出しを介して実装されていました。V8 v9.0以降、非最適化コードでインラインキャッシュ (IC)システムを再利用し、ランタイムへのジャンプなしで適切な最適化コードを生成するようになりました。

以下のグラフからわかるように、以前はランタイム呼び出しのため、superプロパティのアクセスは通常のプロパティアクセスよりも桁違いに遅かったですが、現在ではかなり近づいています。

superプロパティアクセスと通常プロパティアクセスの比較(最適化済み)

superプロパティアクセスと通常プロパティアクセスの比較(非最適化)

superプロパティアクセスはベンチマークが難しいです。関数内部でのみ発生するため、個別のプロパティアクセスではなく、より大きな作業単位で測定する必要があります。したがって、関数呼び出しのオーバーヘッドも測定に含まれます。上記のグラフはsuperプロパティアクセスと通常のプロパティアクセス間の差をやや過小評価していますが、旧アクセス方法と新しいアクセス方法の違いを示すには十分正確です。

非最適化(インタプリテッド)モードでは、superプロパティアクセスは通常のプロパティアクセスより常に遅くなります。これは、(コンテキストからのホームオブジェクトの読み取り、ホームオブジェクトからの__proto__の読み取りといった)追加の読み取り操作が必要なためです。最適化コードでは、可能であればホームオブジェクトを定数として埋め込みます。これをさらに改善するために、その__proto__も定数として埋め込むことが可能です。

プロトタイプ継承とsuper

まず基本から始めましょう。superプロパティアクセスとは何を意味するのでしょうか?

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

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

ここではABのスーパークラスであり、b.m()は予想どおり100を返します。

クラス継承の図

JavaScriptのプロトタイプ継承の実際はさらに複雑です。

プロトタイプ継承の図

__proto__prototypeプロパティを慎重に区別する必要があります。この2つは同じものではありません!さらに混乱を招くことに、オブジェクトb.__proto__はしばしば「bのプロトタイプ」と呼ばれます。

b.__proto__は、bがプロパティを継承するオブジェクトです。一方、B.prototypenew B()で作成されたオブジェクトの__proto__となるオブジェクトです。つまり、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はこれらのオブジェクトのいずれかに定義されているすべてのプロパティにアクセスできます。メソッドmB.prototypeのプロパティ(B.prototype.m)です—そのため、b.m()は機能します。

ここで、super.xをメソッドm内で、ホームオブジェクトの__proto__でプロパティxを検索し、プロトタイプチェーンを上にたどるプロパティ検索として定義できます。

ホームオブジェクトとはメソッドが定義されているオブジェクトです。この場合、mのホームオブジェクトはB.prototypeです。その__proto__A.prototypeであり、ここからプロパティxを探し始めます。この場合、検索開始オブジェクトで即座にプロパティxが見つかりますが、一般的にはプロトタイプチェーンのさらに上で見つかる場合もあります。

もしB.prototypexという名前のプロパティがあったとしても、それは無視されます。なぜなら、プロトタイプチェーンでその上から検索を始めるからです。また、この場合、superプロパティの検索はレシーバ(メソッドを呼び出す際のthis値)には依存しません。

B.prototype.m.call(some_other_object); // 依然として100を返します

ただし、プロパティにゲッターがある場合、レシーバはゲッターにthis値として渡されます。

まとめると:superプロパティアクセス(super.x)では、検索開始オブジェクトはホームオブジェクトの__proto__で、レシーバはsuperプロパティアクセスが発生するメソッドのレシーバです。

通常のプロパティアクセスo.xでは、オブジェクトoでプロパティxを探し始め、プロトタイプチェーンをたどります。また、xにゲッターが存在する場合、oがレシーバーとして使用されます。つまり、検索開始オブジェクトとレシーバーは同じオブジェクト(o)です。

スーパープロパティアクセスは、検索開始オブジェクトとレシーバーが異なるという点を除けば、通常のプロパティアクセスと同様です。

より速いsuper実装

上記の理解は、速いスーパープロパティアクセスの実装の鍵でもあります。V8はすでにプロパティアクセスを高速化するよう設計されており、レシーバーと検索開始オブジェクトが異なる場合にもこれを一般化しました。

V8のデータ駆動型インラインキャッシュ(IC)システムは、速いプロパティアクセスを実現するための中核的な部分です。詳しくは、上記リンク先の高レベルな説明や、V8のオブジェクト表現、およびV8のデータ駆動型ICシステムの実装に関する詳細をご覧ください。

superを高速化するために、新しいIgnitionバイトコードLdaNamedPropertyFromSuperを追加しました。これにより、インタープリタモードでICシステムに組み込むことが可能となり、スーパープロパティアクセス用の最適化コードを生成できるようになりました。

新しいバイトコードにより、スーパープロパティの読み取りを高速化するために新しいICLoadSuperICを追加できます。通常のプロパティ読み取りを処理するLoadICと同様に、LoadSuperICはこれまでに見た検索開始オブジェクトの形状を追跡し、それらの形状のオブジェクトからプロパティを読み取る方法を記憶します。

LoadSuperICは既存のプロパティ読み取り用IC機構を再利用しますが、異なる検索開始オブジェクトを用います。ICレイヤーがすでに検索開始オブジェクトとレシーバーを区別していたため、実装は簡単であるべきでした。ただし、検索開始オブジェクトとレシーバーが常に同じであることを前提とした場合、意図せず検索開始オブジェクトを使用したり、その逆をするバグが発生しました。これらのバグは修正され、現在では検索開始オブジェクトとレシーバーが異なるケースを正しくサポートしています。

スーパープロパティアクセスの最適化コードは、TurboFanコンパイラのJSNativeContextSpecializationフェーズによって生成されます。この実装では、既存のプロパティ検索機構(JSNativeContextSpecialization::ReduceNamedAccess)を一般化して、レシーバーと検索開始オブジェクトが異なるケースを処理します。

さらに最適化コードは、JSFunctionに保存されていたホームオブジェクトを移動させたときにさらに効率的になりました。現在はクラスコンテキストに保存されており、TurboFanは可能な限りそれを定数として最適化コードに埋め込むことができるようになりました。

superのその他の使用例

オブジェクトリテラルのメソッド内でのsuperは、クラスメソッド内でのsuperと同様に動作し、同様に最適化されています。

const myproto = {
__proto__: { 'x': 100 },
m() { return super.x; }
};
const o = { __proto__: myproto };
o.m(); // returns 100

もちろん、最適化されていない特殊なケースもあります。例えば、スーパープロパティの書き込み(super.x = ...)は最適化されていません。また、ミックスインを使用するとアクセス場所がメガモーフィックになり、スーパープロパティアクセスが遅くなります:

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

すべてのオブジェクト指向パターンが可能な限り高速になるようにするためには、まだやるべきことがあります。今後の最適化にご期待ください!