슈퍼 빠른 super 속성 접근


원문 : Super fast super property access / 저자: Marja Hölttä - (https://twitter.com/marjakh) 라이선스: CC BY 3.0

super 키워드는 부모 객체의 함수나 속성에 접근하는 데 사용된다. 이전에는, 상위 속성으로 접근하는 것(super.x와 같은)은 런타임 실행으로 구현되어있었다. V8 v9.0부터는 최적화되지 않은 코드에 인라인 캐시(IC) 시스템을 재사용하여, 상위 속성에 접근하는 코드를 적절히 최적화된 코드를 생성한다. 이제 런타임 실행을 하지 않는다.

아래 그래프를 보면 알겠지만, 상위 속성 접근은 런타임에서 실행되기 때문에, 일반 속성 접근보다 느리곤 했다. 하지만 지금의 상위 속성 접근은 일반 속성 접근과 거의 대등해져 가고 있다.

상위 속성 접근과 일반 속성 접근 비교, 최적화된 상태

상위 속성 접근과 일반 속성 접근 비교, 최적화되지 않은 상태

상위 속성 접근은 함수 내부에서 일어나므로 벤치마킹하기가 어렵다. 개별 속성 접근을 벤치마킹할 수도 없지만, 더 큰 덩어리의 작업은 벤치 마크할 수 있다. 그러므로 함수 호출 오버헤드도 측정에 포함된다. 위의 그래프는 상위 속성 접근과 일반 속성 접근의 차이는 근소해 보이지만, 이전 버전의 상위 속성 접근과 새로운 상위 속성 접근의 차이를 보여주기에는 충분하다.

비 최적화(인터프리터) 모드에서는 컨텍스트의 홈 객체를 읽고 컨텍스트의__proto__를 읽는 등 여러 번 읽는 작업을 거쳐야 하므로, 상위 속성 접근이 일반 속성 접근보다 항상 느리다. 최적화된 코드에서는 홈 객체를 언제든 상수로 사용할 수 있게 내장되어있다. 이로 인해 홈 객체의 __proto__또한 상수화 된다.

프로토타입 상속과 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을 반환한다.

클래스 상속 다이어그램

클래스 상속 다이어그램

하지만, 자바스크립트의 프로토타입 상속의 현실은 훨씬 더 복잡하다.

프로토타입 상속 다이어그램

프로토타입 상속 다이어그램

여기서 __proto__prototype 속성이 서로 다르다는 것을 알아야 한다. 둘은 절대 같은 것이 아니다. 더 헷갈리게 하는 것은, 대부분 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는 이런 객체들의 모든 정의된 속성에 접근이 가능하다. b.m()이 왜 동작하는지 설명해보자면, 메서드 mB.prototype의 속성이므로, B.prototype.m인 것이다. 이제, m 내부의 super.x 는 속성 조회(lookup)라는 것을 알 수 있다. x를 찾을 때까지 홈 객체의 __proto__와 프로토타입 체인을 거슬러 올라가는 것이다. 여기서 말하는 홈 객체는 메서드가 정의된 객체다. 지금의 경우, m의 홈 객체는 B.prototype이다. 홈 객체 자신의 __proto__A.prototype이다. 그러므로 A.prototype이 속성 x를 찾기 시작해야 하는 곳이다. A.prototype를 조회 시작 객체다. 이번에는 x를 조회 시작 객체에서 바로 발견했다. 하지만, 보통은 프로토타입을 더 거슬러 올라가야 발견할 수 있다. 만약 B.prototypex라는 속성을 가지고 있더라도, 프로토타입 체인 상위로 찾아 나가야 하므로 무시하면 된다. 또한, 지금의 경우 상위 속성 조회는 메소드를 호출할 때 this 값이 되는 _ 수신자(reciever)_ 에 의존하지 않는다.

B.prototype.m.call(some_other_object); // 여전히 100을 반환한다.

만약 그 속성에 getter가 있다면, 수신자가 getter에 this 값으로 전달된다. 지금까지의 설명을 종합해보면, 상위 속성 접근은, super.x가 있을 때, 홈 객체의 __proto__부터 조회가 시작되고, 수신자는 상위 속성 접근이 일어나는 메서드의 수신자다. 일반적인 속성 접근은, o.x가 있을 때, ox라는 속성이 있는지 조회하기 시작해서 프로토타입 체인을 거슬러 올라간다. 그리고 x에 getter가 있을 때, o를 수신자로 사용한다. 조회 시작 객체와 수신자는 동일한 객체다. (o객체)

상위 속성 접근은 일반적인 속성 접근과 비슷하지만, 조회 시작 객체와 수신자가 다르다.

빠른 super만들기

위에서 얻은 고찰이 빠른 상위 속성 접근을 구현하기 위한 열쇠기도 하다. V8은 이미 더 빠른 속성 접근을 할 수 있게 개량되었다. 이제 수신자와 조회 시작 객체가 다른 경우만 일반화시키면 된다.

V8의 데이터 주도 인라인 캐시 시스템은 빠른 속성 접근을 개발하기 위한 코어다. 이 링크를 통해 V8의 객체 서술V8이 어떻게 데이터 주도 인라인 캐시 시스템을 구현했는지 더 자세한 소개를 읽어볼 수 있다.

super를 빠르게 만들기 위해, 새로운 Ignition 바이트 코드 LdaNamedPropertyFromSuper를 추가했다. 이를 통해 인터프리터 모드에서도 IC 시스템을 사용할 수 있게 되어서, 상위 속성 접근을 위해 최적화된 코드를 생성할 수 있게 되었다. 이 새로운 바이트 코드로, 상위 속성을 불러오는 속도를 높여주는 새로운 IC인 ** LoadSuperIC를 추가했다. 일반 속성을 불러오는 **LoadIC와 비슷하지만, LoadSuperIC는 계속해서 자신이 본 조회 시작 객체의 모습과 그와 비슷한 모습의 객체에서 속성을 어떻게 불러왔는지 기억해둔다.

LoadSuperIC는 속성을 불러오는 기존 IC 메커니즘을 재사용한다. 단지 다른 조회 시작 객체만 가질 뿐이다. IC 계층이 이미 조회 시작 객체와 수신자를 구분할 수 있으므로, ** LoadSuperIC**를 구현하는 것은 어렵지 않았다. 하지만 조회 시작 객체와 수신자가 항상 같았으므로, 조회 시작 객체 대신 수신자를 사용하는 등의 버그가 있었다. 그리고 그 반대의 버그도 있었다. 이제 그런 버그는 수정되었고, 수신자와 조회 시작 객체가 다른 경우를 제대로 지원하게 되었다.

상위 속성 접근을 위한 최적화된 코드는 TurboFan 컴파일러의 JSNativeContextSpecialization 단계에서 생성된다. 기존의 속성 조회 메커니즘(JSNativeContextSpecialization::ReduceNamedAccess)을 일반화한 구현으로 조회 시작 객체와 수신자가 다른 경우를 처리한다.

최적화된 코드는 홈 객체를 저장된 JS함수 밖으로 꺼냈을 때 훨씬 더 최적화되었다. 이제 최적화된 코드는 클래스 컨텍스트에 저장되므로, TurboFan이 가능한 한 항상 최적화한 코드를 상수화해서 포함할 것이다.

super의 다른 사용법들


객체 리터럴 메서드 내부의 super는 클래스 메서드 안에 있는 것처럼 작동하며, 앞의 경우와 비슷하게 최적화된다.

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

최적화하지 못한 코너 케이스도 있다. 예를 들어, super 속성 변경(super.x = ...)은 최적화되지 않는다. 게다가, 믹스인을 사용하는 것은 접근 지점을 megamorphic( 역: https://en.wikipedia.org/wiki/Inline_caching#Megamorphic_inline_caching))으로 바꾼다. 이렇게 되면 상위 속성 접근이 느려지게 된다.

function createMixin(base) {
  class Mixin extends base {
    m() { return super.m() + 1; }
    //                ^ 이 접근 지점은 megamorphic이다.
  }
  return Mixin;
}

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

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

아직 모든 객체 지향 패턴을 원래 의도대로 빠르게 만들기 위해 해야 할 일들이 있다. 다음 최적화 소식도 놓치지 않길 바란다.


저작권 및 라이선스