올리브영 테크블로그 포스팅 자바스크립트 이렇게 짜면 외않되?
Frontend

자바스크립트 이렇게 짜면 외않되?

V8엔진의 동작원리와 최적화 방법에 대해서 알아봅시다

2023.10.28

안녕하세요. 질문을 사랑하는 올리브영 프론트엔드 개발자 “우문Hyun답”입니다.😁

많은 개발자분들이 코드를 어떻게 작성해야 조금 더 빠르게 동작시킬 수 있을까 라는 고민을 많이 하실 텐데요

이번 포스트에서는 V8 엔진은 무엇이고 V8 엔진이 좋아하는(?) 자바스크립트 코드에 대해 알아보고자 합니다.



V8엔진이란?

V8은 google에서 개발한 오픈 소스 자바스크립트 엔진으로 웹 브라우저에서 자바스크립트 코드를 실행하는 데 사용되며 C++로 작성되었습니다. 현재 chrome, safari를 포함한 여러 브라우저가 V8 엔진을 사용하고 있습니다.

V8은 속도 향상을 위해 JIT(Just In Time) 컴파일러를 사용해 코드를 실행하기 전에 기계어로 번역하여 실행하는데 기계어 코드는 컴퓨터의 하드웨어에서 직접 실행되므로 속도 측면에서 이점이 있습니다.

JIT는 실행 중에 최적화가 가능한 코드를 터보팬(turbo fan)을 이용해 최적화할 수 있는데 이번 포스팅에서 JIT 최적화의 이점을 얻으려면 어떻게 코드를 작성해야 하는지에 대해 중점적으로 얘기하고자 합니다.

JIT의 동작 원리

프로그램을 실행할 때 인터프리터를 사용해 일부 코드를 실행하면서 반복되는 코드를 컴파일하여 기계어로 변환하는 방식인데

  1. 낱말 분석 과정을 통해 코드를 토큰화합니다. parser에서 분해된 토큰을 바탕으로 tree를 생성하는데 이를 추상 구문트리(Abstract Syntax Tree, AST)라고 합니다.
  2. 인터프리터를 사용해 AST를 기반으로 코드를 실행합니다
  3. 인터프리터는 코드 실행 과정에서 프로파일링 정보를 수집합니다. (함수 호출 빈도, 변수 타입 등)
  4. 프로파일링 정보를 기반으로 JIT 컴파일러가 중간 표현 (Intermediate Representation, IR)을 생성합니다.
  5. 터보팬에서는 IR 그래프를 분석해 최적화는 시도합니다
  6. 이 과정에서 인라인 캐싱, 히든클래스 등의 최적화 기술이 사용됩니다.
  7. 최적화된 IR 코드가 기계코드로 컴파일되고 메모리에 로드됩니다.
  8. 기계코드를 실행합니다.


인라인 캐싱과 히든클래스

터보팬에서 인라인 캐싱과 히든클래스 기술을 통해 최적화를 한다고 말씀드렸습니다.

그럼 이제 인라인 캐싱과 히든클래스가 무엇인지 알아보도록 하겠습니다.

인라인 캐싱이란?


호출 지점에서 이전의 메소드 검색의 결과를 기억해서 런타임 메소드 바인딩의 성능을 높이는 것인데 함수를 호출할 때 같은 call site에서는 계속해서 같은 함수를 불러올 가능성이 크다는 관찰을 기반으로 합니다.

V8 엔진은 메소드 호출에 파라미터로 전달된 객체 타입의 캐시를 유지하고 이 정보를 이용해 앞으로 파라미터로 넘어올 객체의 타입에 대해 가정을 합니다.

이게 무슨말이지..❓🙃❓

아래 코드 예시와 함께 이해해 보겠습니다.

// 같은 함수를 5억번 실행하는 코드입니다.

// [A] 최적화되지 않은 예시
(() => {
  const jongu = { firstname: "Jongu", lastname: "Lee", job: "developer" };
  const minwoo = { firstname: "Minwoo", lastname: "Park", age: 31 };
  const hyunwoo = { firstname: "Hyunwoo", lastname: "Choi", gender: "male" };
  const yujin = { firstname: "Yujin", lastname: "Kwon", hobby: "drawing" };
  const minji = { firstname: "Minji", lastname: "Kim", nickname: "MJ" };

  // 사람의 이름을 출력하는 함수
  const getFullName = (user) => `${user.lastname} ${user.firstname}`;

  const people = [jongu, minwoo, hyunwoo, yujin, minji];

  console.time("최적화 되지 않은 코드");
  for (let i = 0; i < 500_000_000; i++) {
    getFullName(people[i % 5]);
  }
  console.timeEnd("최적화 되지 않은 코드");
})();
// 결과 - 최적화 되지 않은 코드: 10.085s

// [B] 최적화된 예시
(() => {
  const jongu = { firstname: "Jongu", lastname: "Lee" };
  const minwoo = { firstname: "Minwoo", lastname: "Park" };
  const hyunwoo = { firstname: "Hyunwoo", lastname: "Choi" };
  const yujin = { firstname: "Yujin", lastname: "Kwon" };
  const minji = { firstname: "Minji", lastname: "Kim" };

  // 사람의 이름을 출력하는 함수
  const getFullName = (user) => `${user.lastname} ${user.firstname}`;

  const people = [jongu, minwoo, hyunwoo, yujin, minji];

  console.time("최적화된 코드");
  for (let i = 0; i < 500_000_000; i++) {
    getFullName(people[i % 5]);
  }
  console.timeEnd("최적화된 코드");
})();
// 결과 - 최적화된 코드: 5.784s

위 코드 A, B의 다른 점은 사용자 객체의 구조뿐입니다.

우리가 집중해야 할 부분은 속성 한 개가 추가됨으로 인해서 속도는 약 2배가량의 차이를 보인다는 점입니다.

인라인 캐시는 최적화 과정에서 객체의 attribute에 접근하는 부분에 실제 메모리 주소를 할당하여 lookup 과정을 생략합니다. 따라서 반복적으로 접근하는 객체의 구조가 동일해야 객체를 구분하는 과정을 생략하고 offset을 적용하여 최적화에 용이합니다.

위 코드에서 user.lastname의 attribute 접근 과정에서 해당 object에서 lastname과 firstname 이라는 attribute의 위치를 offset으로 저장하여, static 하게 최적화 코드에 넣어버리기 때문입니다.

히든클래스란?


자바스크립트는 동적 언어이며 객체가 생성된 이후에도 속성을 쉽게 추가하거나 삭제할 수 있습니다.

히든클래스는 객체지향 언어에서 사용되는 고정 객체 레이아웃과 유사하게 작동하는데, 런타임에 생성된다는 차이점이 있습니다. V8 엔진은 히든클래스를 활용하여 객체의 프로퍼티 구조를 효율적으로 처리합니다.

터보팬은 히든 클래스 정보를 활용하여 객체에 대한 프로퍼티 접근을 최적화합니다.

..❓🙃❓

코드 예시를 통해 알아봅시다.

function Point(x, y) {
    this.first = x;             // B
    this.second = y;            // C
}

const p1 = new Point(1, 2);     // A
  1. A 과정에서 V8 엔진은 C0 히든클래스를 생성하고, p1 객체는 C0를 참조합니다.

    A

  1. B 과정에서 V8 엔진은 C0를 기반으로 C1이라는 두 번째 히든클래스를 생성합니다.
  • property table: 프로퍼티가 메모리의 어디에 있는 찾기 쉽게 알려주는 이정표

  • transition table: 구조가 변하면 어디로 가야 하는지 알려주는 이정표

    B

  1. C 과정에서 V8 엔진은 C2 히든클래스를 만들고 클래스 전환을 통해서 업데이트 합니다.

    C

위와 같은 과정을 거치며 히든클래스들이 생성되는데 개발 시 주의할 점은 새로운 히든 클래스를 생성할 때마다 기존에 생성된 히든클래스를 이용할 수 있도록 코드를 작성해야 한다는 점입니다.

아래 코드는 히든클래스 관점에서 좋지 않은 코드인데

function Point(x, y) {
    this.first = x;
}

const p1 = new Point(1);
const p2 = new Point(2);

p1.second = 10;
p1.third = 100;

p2.third = 100;
p2.second = 10;

결과적으로 p1과 p2는 동일한 데이러를 가지고 있지만 서로 다른 히든 클래스를 참조하게 됩니다.

위와 같은 코드는 같은 offset을 참조하지 못하므로 인라인 캐싱이 되지 않고 여러 개의 히든클래스를 만들게 되는 문제를 초래합니다.



그럼 어떻게 코드를 짜야해?

이번 포스팅에서 정리한 내용을 토대로 자바스크립트 코드를 작성할 때 어떤 점을 주의해야 하는 지를 정리해보겠습니다.

  1. 객체 속성을 항상 같은 순서로 초기화해서 히든 클래스 및 이후에 생성되는 최적화 코드가 공유될 수 있도록 합니다.
  2. 인라인 캐싱을 위해 비슷한 유형의 객체를 사용해 여러 개의 히든클래스를 만드는 것이 아닌 히든클래스를 최소화하여 오프셋을 통한 메모리 참조를 최대화하도록 해야 합니다.
  3. 동일한 메소드를 반복적으로 수행하는 코드가 서로 다른 메소드를 한 번씩 수행하는 코드보다 빠르게 동작하는 것을 숙지해야 합니다.


마치며

당연히 동작하는 코드는 없다는 관점을 시작으로 해당 블로그를 작성하게 되었습니다.

객체 형식의 데이터를 자주 사용하는 오늘날의 개발 판에서 이해하지 못하고 넘어간다면 실수가 나올 수 있고 그로 인해 사용자 경험을 떨어뜨릴 수 있다고 생각됩니다.

이번 글을 읽으신 분들은 V8 엔진의 동작 방식과 터보팬의 동작을 이해하고 코드를 작성하시리라 믿습니다!

계속해서 성장하여 더 좋은 사용자 경험을 제공할 수 있는 올리브영 프론트엔드 개발자가 되도록 하겠습니다.

긴 글 읽어주셔서 감사합니다!❤️

WebFrontEndOptimization
올리브영 테크 블로그 작성 자바스크립트 이렇게 짜면 외않되?
🏀
우문Hyun답 |
Front-end Engineer
질문을 사랑하는 개발자입니다.