JavaScript V8 engine 최적화 전략 3가지

Namu CHO
13 min readFeb 1, 2024

--

inlining, hidden class, and inline caching

출처: https://v8.dev/logo

Inlining (인라이닝):

Inlining (인라이닝)은 함수 호출의 오버헤드를 줄이기 위해 사용되는 중요한 컴파일러 최적화 기법입니다. 이 과정에서는 함수 호출을 함수의 본문으로 직접 대체합니다. 이러한 최적화는 실행 시간을 단축시키는 데 도움이 됩니다.

인라이닝의 예시:

인라이닝 전:

고려해야 할 함수와 이 함수를 사용하는 코드가 있을 때, 함수 호출은 별도의 호출과 반환 과정을 거칩니다.

function add(a, b) {
return a + b;
}

function main() {
let result = add(5, 3);
console.log(result);
}

위의 예에서, main 함수는 add 함수를 호출합니다. add 함수는 매우 간단하므로 인라이닝을 적용하기에 좋은 예시입니다.

인라이닝 후:

컴파일러 또는 인터프리터는 add 함수의 본문을 직접 main 함수 내부에 삽입할 수 있습니다. 이 경우, add 함수의 호출 부분이 그 함수의 본문으로 대체됩니다.

function main() {
let a = 5;
let b = 3;
let result = a + b; // 인라이닝된 add 함수의 본문
console.log(result);
}

Inlining (인라이닝)은 함수 호출의 오버헤드를 줄이기 위해 사용되는 중요한 컴파일러 최적화 기법입니다. 이 과정에서는 함수 호출을 함수의 본문으로 직접 대체합니다. 이러한 최적화는 실행 시간을 단축시키는 데 도움이 됩니다.

인라이닝의 장단점:

장점:

  • 성능 향상: 함수 호출과 반환에 소요되는 시간을 줄여, 전체 실행 시간이 감소합니다.
  • 오버헤드 감소: 작은 함수의 경우, 호출 자체가 함수 내부 로직보다 더 많은 시간을 소요할 수 있습니다. 인라이닝은 이러한 오버헤드를 제거합니다.

단점:

  • 코드 크기 증가: 인라이닝은 함수의 본문을 여러 곳에 복사하므로, 결과적으로 코드의 크기가 커질 수 있습니다.
  • 메모리 사용 증가: 큰 함수를 인라이닝할 경우, 메모리 사용이 증가할 수 있습니다.
  • 유지 관리의 어려움: 함수 본문이 여러 곳에 중복되므로, 코드를 변경하거나 디버깅할 때 어려움이 있을 수 있습니다.

이러한 이유로, 인라이닝은 주로 작고, 자주 호출되는 함수에 대해 적용됩니다. 컴파일러는 이러한 결정을 자동으로 수행하며, 개발자는 일반적으로 이 과정에 직접 개입하지 않습니다.

Hidden Classes (히든 클래스):

Hidden Classes (히든 클래스)는 JavaScript V8 엔진에서 객체의 속성에 빠르게 접근하기 위해 사용하는 최적화 기법입니다.

JavaScript는 동적 언어이기 때문에, 객체의 속성을 실행 시간에 추가하거나 변경할 수 있습니다. 이러한 동적인 특성은 성능에 영향을 미칠 수 있는데, 히든 클래스는 이를 효율적으로 관리하여 성능을 향상시킵니다.

히든 클래스의 작동 원리:

히든 클래스는 객체의 속성이 추가되거나 변경될 때마다 내부적으로 생성되는 구조체입니다. 이 구조체는 객체의 현재 속성 레이아웃을 나타내며, 같은 속성 레이아웃을 가진 다른 객체와 재사용될 수 있습니다.

코드 예시:

히든 클래스 생성:

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

var p1 = new Point(1, 2);
var p2 = new Point(3, 4);

위 코드에서, Point 생성자 함수를 사용하여 p1p2 객체를 생성합니다. 이때, V8 엔진은 Point 객체에 대한 히든 클래스를 생성합니다. p1p2는 같은 생성자를 사용하므로, 같은 히든 클래스를 공유합니다.

히든 클래스 변경:

p1.color = "red";
var p3 = new Point(5, 6);
p3.color = "blue";

여기서 p1에 새로운 속성 color를 추가하면, V8은 p1의 히든 클래스를 새로운 버전으로 업데이트합니다. 이제 p1p2는 서로 다른 히든 클래스를 가지게 됩니다. p3 객체에 color 속성을 추가하면, p3p1과 같은 업데이트된 히든 클래스를 사용하게 됩니다.

히든 클래스의 장단점:

장점:

  1. 속성 접근 최적화: 히든 클래스를 사용함으로써, 객체의 속성 접근이 빨라집니다. 같은 히든 클래스를 공유하는 객체들은 속성 접근 패턴이 유사하므로, V8 엔진은 이를 효율적으로 처리할 수 있습니다.
  2. 인라인 캐싱 향상: 히든 클래스는 인라인 캐싱과 연계되어, 반복적인 속성 접근이 더 빨라질 수 있습니다.

단점:

  1. 속성 추가 순서의 중요성: 객체에 속성을 추가하는 순서가 성능에 큰 영향을 미칠 수 있습니다. 같은 속성을 가지고 있어도, 이들을 다른 순서로 추가하면 V8 엔진은 각각 다른 히든 클래스를 생성합니다. 이로 인해 추가적인 메모리 사용과 성능 저하가 발생할 수 있습니다.
  2. 동적 속성 변경의 비효율성: 객체에 동적으로 속성을 추가하거나 변경하는 것은 히든 클래스 시스템에 부담을 줄 수 있습니다. 객체의 구조가 자주 바뀌면, V8 엔진은 새로운 히든 클래스를 계속 생성해야 하며, 이는 메모리 사용량을 증가시키고 성능을 저하시킬 수 있습니다.
  3. 메모리 사용 증가: 서로 다른 구조를 가진 많은 객체가 생성될 경우, 각각의 객체 구조에 대해 별도의 히든 클래스가 만들어집니다. 이는 메모리 사용량을 증가시킬 수 있습니다.
  4. 예측 불가능한 성능 변화: 객체의 구조가 변경되면 히든 클래스 또한 변경됩니다. 이러한 변경은 때때로 예측하기 어려운 성능 변화를 야기할 수 있습니다. 예를 들어, 같은 코드가 다른 상황에서는 다르게 최적화될 수 있습니다.
  5. 최적화 복잡성: 히든 클래스는 JavaScript 엔진의 내부적인 최적화 기법이므로, 개발자가 직접적으로 제어하거나 예측하기 어렵습니다. 이로 인해 성능 문제를 진단하고 해결하는 것이 복잡해질 수 있습니다.

이러한 단점들에도 불구하고, 히든 클래스는 JavaScript 객체의 속성 접근을 획기적으로 개선하는 데 큰 도움이 됩니다. 하지만, 이를 효과적으로 활용하기 위해서는 객체의 구조를 가능한 안정적으로 유지하고, 성능에 민감한 부분에서는 객체의 동적 변화를 최소화하는 것이 좋습니다.

히든 클래스는 JavaScript의 동적인 특성과 관련된 성능 문제를 최적화하는 강력한 방법입니다. 개발자는 이러한 내부 메커니즘을 이해함으로써 보다 효율적인 코드를 작성할 수 있습니다.

Inline Caching (인라인 캐싱):

Inline Caching (인라인 캐싱)은 JavaScript 엔진에서 객체의 속성 접근이나 메서드 호출의 성능을 향상시키기 위한 중요한 최적화 기법입니다. 이 기법은 자주 사용되는 객체의 속성이나 메서드 접근 결과를 캐시해두고, 이후 동일한 접근이 발생할 때 빠르게 결과를 제공합니다.

인라인 캐싱의 작동 원리:

인라인 캐싱은 특정 타입의 객체에 대한 속성 접근이나 메서드 호출이 처음 이루어질 때, 그 결과를 캐싱합니다. 같은 타입의 다른 객체에 대해 동일한 접근이 이루어지면, 엔진은 캐시된 결과를 사용하여 빠르게 응답합니다.

코드 예시:

인라인 캐싱 전:

function findArea(shape) {
return shape.width * shape.height;
}

let rectangle1 = { width: 10, height: 5 };
let rectangle2 = { width: 5, height: 3 };

console.log(findArea(rectangle1)); // 첫 번째 호출
console.log(findArea(rectangle2)); // 두 번째 호출

위의 코드에서 findArea 함수는 shape 객체의 widthheight 속성을 사용합니다. 첫 번째 호출과 두 번째 호출은 서로 다른 객체(rectangle1, rectangle2)에 대해 이루어지지만, 같은 속성(width, height)에 접근합니다.

인라인 캐싱 후:

첫 번째 findArea(rectangle1) 호출 시, V8 엔진은 rectangle1widthheight 속성에 대한 접근을 캐시합니다. 두 번째 findArea(rectangle2) 호출이 발생할 때, 엔진은 rectangle2rectangle1과 같은 히든 클래스(즉, 같은 속성 레이아웃)를 가진다는 것을 감지하고, 첫 번째 호출에서 캐싱된 결과를 사용해 빠르게 계산을 수행합니다.

인라인 캐싱의 장단점:

장점:

  1. 성능 향상: 자주 접근하는 속성이나 메서드에 대한 캐싱을 통해 속성 접근 및 메서드 호출이 훨씬 빠르게 이루어집니다.
  2. 연산 비용 절감: 객체 속성의 위치를 매번 계산하는 대신 캐시된 정보를 사용함으로써 연산 비용이 줄어듭니다.
  3. 동적 최적화: JavaScript와 같은 동적 언어에서 성능을 향상시키기 위해 중요한 기법입니다.

단점:

  1. 특정 조건에 최적화된 결과: 인라인 캐싱은 특정 타입의 객체나 특정 구조의 객체에 대해 최적화됩니다. 만약 객체의 구조가 변경되거나 예상과 다른 타입의 객체가 사용될 경우, 최적화된 코드는 더 이상 유효하지 않게 됩니다. 이런 경우, 엔진은 캐시를 무효화하고 다시 최적화 과정을 거쳐야 합니다. 이러한 “디옵티마이징”은 성능에 부정적인 영향을 미칠 수 있습니다.
  2. 메모리 사용 증가: 인라인 캐싱은 성능을 향상시키기 위해 추가적인 메모리를 사용합니다. 각 캐시된 메서드나 속성 접근에 대한 정보를 저장하기 위해 메모리가 필요합니다. 많은 수의 다양한 객체와 메서드가 사용되는 대규모 애플리케이션에서는 이것이 상당한 메모리 사용량으로 이어질 수 있습니다.
  3. 유지보수의 복잡성: 인라인 캐싱은 엔진 내부에서 자동으로 수행되는 최적화 과정입니다. 따라서 개발자가 이를 직접 제어하거나 예측하기 어렵습니다. 이는 성능 문제를 진단하고 해결하는 과정을 복잡하게 만들 수 있습니다.
  4. 변경에 대한 민감성: 객체의 구조나 사용되는 타입이 자주 변경되면, 인라인 캐싱은 효과적이지 않을 수 있습니다. 객체의 구조가 변경될 때마다 엔진은 새로운 최적화를 수행해야 하며, 이는 추가적인 처리 시간을 요구합니다.
  5. 최적화에 대한 오버헤드: 특정 조건에서는 인라인 캐싱으로 인한 성능 향상이 추가적인 오버헤드를 상쇄하지 못할 수 있습니다. 특히, 메서드나 속성 접근이 그리 자주 발생하지 않는 경우에는 인라인 캐싱으로 인한 이점이 크지 않을 수 있습니다.

이러한 최적화 전략들은 JavaScript의 동적인 특성과 느린 부분들을 보완하여 V8 엔진이 빠른 실행 속도를 제공할 수 있도록 돕습니다.

이러한 V8 엔진의 최적화 전략을 이해하고 이를 기반으로 한 몇 가지
개발자 관점에서의 최적화 팁을 공유하겠습니다:

1. 객체의 속성 초기화 순서를 일관되게 유지하기

  • V8 엔진은 객체의 속성이 추가되는 순서에 따라 내부적으로 히든 클래스를 생성합니다. 동일한 속성을 가진 객체라도 속성을 추가하는 순서가 다르면 다른 히든 클래스를 사용하게 됩니다. 이는 추가적인 메모리 사용과 성능 저하를 초래할 수 있습니다. 따라서 객체를 생성하고 속성을 초기화할 때는 항상 동일한 순서를 사용해야 합니다.
// 좋은 예
function createPerson(name, age) {
var person = {};
person.name = name;
person.age = age;
return person;
}

// 나쁜 예
function createPerson(name, age) {
var person = {};
if (name) {
person.name = name;
}
// 조건에 따라 속성을 추가하면 다른 히든 클래스를 생성할 수 있음
person.age = age;
return person;
}

2. 작은 함수를 사용하고 인라이닝을 용이하게 하기

  • V8 엔진은 실행 성능을 향상시키기 위해 자주 호출되는 작은 함수를 인라인 처리합니다. 함수가 너무 크면 인라인 처리가 되지 않을 수 있으므로, 가능한 함수를 작게 유지하고, 한 가지 일만 하도록 설계하는 것이 좋습니다. 이는 코드의 가독성과 유지보수성을 높이는 동시에 성능도 개선할 수 있습니다.

3. 동적 속성 대신 정적 속성 사용하기

  • JavaScript는 매우 유연한 언어로, 실행 시간에 객체에 새로운 속성을 추가할 수 있습니다. 하지만, 객체에 동적으로 속성을 추가하면 V8 엔진이 최적화하기 어렵게 만들 수 있습니다. 가능한 객체의 구조를 생성 시점에 모두 정의하고, 실행 도중에는 변경하지 않는 것이 좋습니다. 이렇게 하면 V8 엔진이 객체의 속성 접근을 더 빠르게 할 수 있습니다.
  • 나쁜 예 코드에서, createPerson 함수는 객체 person에 여러 조건에 따라 동적으로 속성을 추가합니다. 예를 들어, age가 30을 초과하면 isAdult 속성을 추가하고, 나이에 따라 isSenior 속성을 설정합니다. 이러한 동적 속성 추가는 V8 엔진이 객체의 구조를 예측하고 최적화하는 데 어려움을 초래합니다.
    각 객체는 동일한 생성자 함수(createPerson)를 통해 생성되었음에도 불구하고, 속성이 조건에 따라 다르게 추가되기 때문에, V8 엔진은 이들을 다른 히든 클래스로 처리하게 됩니다. 이는 추가적인 메모리 사용과 성능 저하를 야기할 수 있으며, 객체의 속성 접근 속도도 느려질 수 있습니다.
    이와 같은 이유로, 객체의 구조는 가능한 한 생성 시에 정의하고, 실행 중에는 변경하지 않는 것이 좋습니다.
// 좋은 예
function createPerson(name, age) {
return {
name: name,
age: age
};
}



// 나쁜 예
function createPerson(name, age) {
var person = {};
person.name = name;

// 조건에 따라 동적으로 속성을 추가
if (age > 30) {
person.isAdult = true;
}

// 함수 실행 중에 새로운 속성을 추가
person.age = age;
person.isSenior = age > 60 ? true : false;

return person;
}

var youngPerson = createPerson("Alice", 25);
var oldPerson = createPerson("Bob", 65);

이러한 최적화 팁은 V8 엔진 뿐만 아니라 다른 JavaScript 엔진에서도 일반적으로 성능을 향상시킬 수 있습니다. 코드를 작성할 때 이러한 원칙을 염두에 두고, 성능과 가독성, 유지보수성 사이의 적절한 균형을 찾는 것이 중요합니다.

--

--