[JavaScript] Optimization - Inline Caching & Hidden Class

dididb·2021년 1월 2일
0

Javascript

목록 보기
2/5

JavaScript Engine(이하 JSE)인 V8의 Ignition이 Interpreting으로 Bytecode를 생성하고 실행하면서 Profiling data와 함께 TurboFan으로 Bytecode를 보내는 것을 알았다. 여기서 TurboFan의 역할은 최적화된 Machine Code를 만들어내는 것이다. 어떻게 만드는 것일까?

그 과정인 Inline Caching과 Hidden Class에 대해서 알아보자.

Inline Caching

말 그대로 캐싱이다. 효율성을 위해 자주 사용하는 데이터를 빠른 속도로 접근하기 쉬운 곳에 저장해두고 사용하는 것이다. 아래 코드를 보자.

function getName(obj) {
    return `This is ${obj.name}`
}

getName({name: "DevJoong"})

이 코드가 반복된다면 getName이라는 함수에 의해 This is DevJoong 이라는 문구가 계속 만들어질것이다. 똑같은 내용이 리턴되는데 반복해서 객체에서 name을 찾을 필요가 없으므로 compiler(TurboFan)은 This is ${obj.name}This is DevJoong으로 문자열을 대체하여 optimized code를 만든다.

Hidden Class

이번에도 코드로 시작해보자.

$ cat test1.js
const sharp = {id: 1, price: 1500};
const pen = {id: 2, price: 2000};
const pencil = {id: 3, price: 500};
const tape = {id: 4, price: 2500};
const clip = {id: 5, price: 1000};

const product = [
    sharp, pen, pencil, tape, 
    clip
];

const getPrice = (obj) => obj.price;

console.time("engine");
for(var i = 0; i < 1000 * 1000 * 1000; i++) { 
    getPrice(product[i & 4]); 
}
console.timeEnd("engine");

위의 코드에서 sharp, pen, pencil, tape, clip 모두 id와 price라는 두 개의 property로 이루어진 object이다. JSE에는 코드를 실행하면 이것을 보고

property information 1 = {
	첫 번째 값(id) 위치
    ...
}

property information 2 = {
	두 번째 값(price) 위치
    ...
}

와 같이 이 property의 정보를 Hidden Class로 만들어서 저장해둔다. 그리고 데이터는 별도로

const sharpObj = {1, 1500}
const penObj = {2, 2000}
const pencilObj = {3, 500}
const tapeObj = {4, 2500}
const clipObj = {5, 1000}

와 같이 attribute만 별도로 저장해둔다. 즉, sharp.price에 접근이 필요한 경우 property information 1에서 id라는 property의 위치를 확인 후 sharpObj의 해당 위치의 값을 가져오는 것이다.

그렇다면 id와 price의 순서를 바꾼 test2를 작성해서 걸리는 시간을 비교해보자.

$ cat test2.js
const sharp = {id: 1, price: 1500};
const pen = {id: 2, price: 2000};
const pencil = {id: 3, price: 500};
const tape = {price: 2500, id: 4};
const clip = {price: 1000, id: 5};
...
$ node test1.js
engine: 737.270ms

$ node test2.js
engine: 864.426ms

내용이 모두 같지만 이 적은 내용에도 고작 id와 price의 순서를 바꿨다는 이유 하나만으로 수행 시간이 무려 100ms가 넘게 차이난다.

이것은 두 test1, 2의 Hidden Class를 생각해보면 된다. test1에서는 Hidden Class를 두개만 만들어 두고 계속 같은 위치의 값을 불러올 수 있었다. 하지만 test2에서는 id와 price의 순서가 바뀐 object가 존재했기 때문에 Hidden Class를 더 많이 만들었어야 했다. 즉, pen.pricetape.price를 찾을 때 다른 Hidden Class를 이용해야 했다는 말이다. 그러므로 왔다갔다 하면서 위치를 찾아야 하니까 속도가 느려질 수 밖에 없었다.

최적화를 위해 사용되는 Hidden Class가 있기에 어떻게 구현하느냐에 따라 같은 내용의 코드도 성능이 많이 달라질 수 있는 것이다.

위의 두 예제는 다음 코드로 object의 property가 더 늘어난다고 해도 성능을 유지시킬 수 있다.

class Product {
    constructor(
        id,
        price,
        count,
        description,
        etc,
    ) {
        this.id = id || 0,
        this.price = price || 0,
        this.count = count || 0,
        this.description = description || "",
        this.etc = etc || ""
    }
}

const sharp = new Product({id: 1, price: 1500});
const pen = new Product({id: 2, count: 10});
const pencil = new Product({id: 3, description: "this is pencil"});
const tape = new Product({id: 4, price: 2500});
const clip = new Product({id: 5, price: 1000});

const product = [
    sharp, pen, pencil, tape, 
    clip
];

const getPrice = (obj) => obj.price;

console.time("engine");
for(var i = 0; i < 1000 * 1000 * 1000; i++) { 
    getPrice(product[i & 4]); 
}
console.timeEnd("engine");

더 디테일하고 정확한 내용은 아래서 확인 가능합니다.
출처: JavaScript engine fundamentals: Shapes and Inline Caches

0개의 댓글