[JS] Optimizing: Hidden Class & Inline Caching

colki·2021년 5월 4일
2

Udemy_JavaScript: The Advanced JavaScript Concepts (2021) 강의를 바탕으로 메모한 내용입니다.

자바스크립트는 동적 프로그래밍 언어로써 런타임중에도 인스턴스화 후에 객체에서 속성을 추가하거나 제거할 수 있다.
또한 자바스크립트에서는 속성값의 위치를 메모리에 저장하기 때문에 속성값을 검색하는데 시간이 매우 오래 걸린다.

var car = function(make,model) {
	this.make = make;
	this.model = model;
}

var myCar = new car(honda,accord);

myCar.year = 2005;

비동적 언어인 Java는 객체의 속성사이의 고정된 오프셋 길이를 기반으로 위치를 기억하기 때문에, 단일 명령으로 속성의 위치를 빠르게 확인할 수 있지만,

속성을 조회할 때 여러가지 명령이 필요한 자바스크립트는 타언어보다 속성 조회속도가 현저히 느릴수 밖에 없다.

그래서 V8 엔진이 최적화를 위해 자바스크립트에도 오프셋 개념을 도입한 Inline Caching과 Hidden Class라는 두가지 테크닉을 사용하게 된 것이다.

두 테크닉들은 서로 밀접하게 연관되어 있어서 전체적으로 이해하고 세부적으로 깊게 보는 것이 그나마 이해하기에 수월하다.

Hidden Class

Reference https://richardartoul.github.io/


Hidden Class란 객체의 프로퍼티가 저장된 주소에 대한 상대적인 위치 정보를 기억하고 있다가 프로퍼티가 동적으로 생성되거나 변경될 때, 함께 업데이트되는 숨겨진 클래스라고 생각하면 된다.

만약 객체가 프로퍼티를 가지고 있지 않다면, Hidden Class 또한 아무런 정보를 가지고 있지 않다.

😮 객체가 만들어질 때 무조건 하나의 Hidden Class가 생성되는데
이때 위치정보를 기억하기 위해 JAVA가 사용하는 오프셋 (offset)의 개념을 자바스크립트에 적용해서 정적인 주소에 접근하듯 빠르게 속성값을 조회할 수 있는 것이다.

오프셋(offset)이란 상대적인 주소를 뜻하는데,
기준이 되는 주소에 상대적인 거리(주소) 값을 더한 것이라고 생각하면 된다.

예를 들어서 ABC가 100번지일때, ABC +50 은 150번지를 뜻하게 된다. 이때 오프셋은 50이 된다.



😮 속성이 업데이트 되고 Hidden Class 역시 업데이트될 때, 기존의 Hidden Class를 참조하기 때문에 연쇄적인 연결고리가 생성된다.

다음 예제를 보자.

function Point(x,y) { // 함수선언 C0 生
  this.x = x; // C0기반의 새로운 C1 生
  this.y = y;// C1기반의 새로운 C2 生
}

var obj = new Point(1,2);

새로운 함수가 선언됐다. 자바스크립트는 Hidden Class C0을 만든다.

this.x 가 실행되면 C0을 기반으로 한 Hidden Class C1이 생긴다.

최초의 Hidden Class C0 에서 Hidden Class C1로 전환된 것이다.

this.y 가 실행되면 다시금 C1을 기반으로 한 Hidden Class C2이 생기면서 업데이트 된다.

객체에 속성이 추가될 때 이전에 있던 Hidden Class에서 새로운 Hidden Class로 업데이트 된다고 볼 수 있다.

또한 업데이트 된 기록도 가지고 있어서 원하는 속성을 조회할 때 빠르게 찾아갈 수 있는 것이다.

이렇게 Hidden Class를 공유하는 방식은 최적화에 있어서 굉장히 중요하다.



😮 숨겨진 클래스 전환은 속성이 개체에 추가되는 순서에 따라 달라진다.

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

const obj1 = new Animal(1, 2);
const obj2 = new Animal(3, 4);

// a, b
obj1.a = 30;
obj1.b = 100;

// b, a
obj2.b = 30;
obj2.a = 100

a와 b는 동일한 Hidden Class를 공유하는 것처럼 보이지만,
a와 b의 순서가 다르게 추가 되어 있다.

값을 순서대로 할당하지 않으면 서로 다른 새로운 히든클래스를 가지게 된다.
이 경우 기존에 가지고 있던 메서드의 최적화에 영향을 주어서 속도가 느려진다.

V8엔진은 Inline Cashing 방법으로 Hidden Class가 오프셋을 저장하는 프로세스이기 때문에, 동일한 순서로 객체의 속성에 접근하여 최적화할 수 있도록 해야 한다.

obj1.a = 30;
obj1.b = 100;

obj2.a = 100
obj2.b = 30;

이렇게 순서대로 속성을 할당해줘야 한다.


Inline Caching


동일한 객체에 동일한 메서드가 반복적으로 실행될 때 Hidden Class를 이용해서 속성에 접근하는 시간을 최적화하는 테크닉이다.

V8엔진은 같은 객체가 매개변수로 같은 메서드에 전달되는 상황을 가정하고 캐시에 정보를 담는다. 반복 호출을 캐치한 뒤에 동일한 메서드의 호출시간을 줄이기 위해,
V8엔진은 Hidden Class 의 offset을 조회하고, offset을 객체의 포인터에 추가한다.

그리고 재호출이 일어났을 때, V8 엔진은 Hidden Class가 변경되지 않았다고 가정하고 이전 조회에서 저장된 오프셋을 사용하여 다이렉트로 특정 속성의 메모리 주소로 점프한다. 그래서 빠른 속도로 속성에 접근할 수있는 것이다!

🔔 POINT

  • 항상 동일한 순서로 객체 속성을 인스턴스화하여 Hidden Class와 최적화를 유지해야 한다.
  • 인스턴스화 후 객체에 속성을 추가하면 Hidden Class가 강제로 변경되고 이전 히든 클래스에 최적화 된 모든 메서드의 속도가 느려진다. 대신 생성자에서 개체의 모든 속성을 할당한다.
  • 동일한 메서드를 반복적으로 실행하는 코드는 Inline Cashing으로 인해 여러 메서드를 한 번만 실행하는 코드보다 빠르게 실행된다.

그래서 Hidden Class에서 살펴본 예제처럼 같은 Hidden Class를 공유하고 있던 두 객체가 서로 다른 Hidden Class를 가지게 하는 순간 이 Inline Caching 테크닉이 먹히지 않는 것이다.

그렇게 되면 V8은 최적화를 해제하고, 원래의 메서드 호출 방법대로 동작하도록 원점으로 되돌려 버린다.

How to Optimize Code?


also for machines the more predictable your code is the better it will be because they'll have no surprises.
And throughout the course we're going to learn these practices that will help us write optimized code

인간 뿐 아니라 컴퓨터가 예측할 수 있는 흐름의 코드를 작성해야 한다.
예측이 가능할 수록 최적화가 잘 되서 빠르고 효율적인 코드가 되는 것이다.

Write Optimized Code


최적화코드를 위해서 아래 키워드들은 정말정말 매우매우 조심해서 사용 해야 한다.
We want to write code in a way that helps the compiler make optimizations.

강의에서는 각 키워드들에 대한 설명이 부족해서 추가로 검색해봤다.

eval()

MDN을 살펴보면 eval의 사용을 극구말리고 있다.

그런데 스택오버플로우에서 솔루션으로 eval를 제시하는 걸 굉장히 많이 봤다..? 🤔

eval()은 인자로 받은 코드를 caller의 권한으로 수행하는 위험한 함수입니다. 악의적인 영향을 받았을 수 있는 문자열을 eval()로 실행한다면, 당신의 웹페이지나 확장 프로그램의 권한으로 사용자의 기기에서 악의적인 코드를 수행하는 결과를 초래할 수 있습니다
또한 최신 JS 엔진에서 여러 코드 구조를 최적화하는 것과 달리
eval()은 JS 인터프리터를 사용해야 하기 때문에 다른 대안들보다 느립니다.

최신 JavaScript 인터프리터는 자바스크립트를 기계 코드로 변환합니다. 즉, 변수명의 개념이 완전히 없어집니다. 그러나 eval을 사용하면 브라우저는 기계 코드에 해당 변수가 있는지 확인하고 값을 대입하기 위해 길고 무거운 변수명 검색을 수행해야 합니다. 또한 eval()을 통해 자료형 변경 등 변수에 변화가 일어날 수 있으며, 브라우저는 이에 대응하기 위해 기계 코드를 재작성해야 합니다.

with

with는 사용해보지 않은 키워드라 잘 모르겠지만,
MDN 에서도 사용을 권장하지 않는다. 잘은 모르겠지만 객체의 속성에 관여하기 때문에 최적화에 영향을 주는 게 아닐까 싶다.

Use of the with statement is not recommended, as it may be the source of confusing bugs and compatibility issues.


=> eval과 with는 스코프와 스코프 체인이 내부적으로 작동하는 방식을 변경시킬 수 있기 때문에 컴파일러가 스코프체인을 연결하고 이해하는 과정에 영향을 줄 수 있으므로 최적화에 문제를 일으킬 수 있다. (scope강의에서 잠깐 언급을 했는데 정확한 원리는 잘 모르겠다..!)

arguments

Avoid modifying or passing 'arguments' into other functions — it kills optimization

arguments는 array-like이기 때문에 배열 메서드를 바로 사용할 수 없어서, 보통 아래와 같이 배열로 변환한 후에 배열 메서드를 쓰는 것이 매우 일반적이다.

// bad
var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);

The arguments object must not be passed or leaked anywhere.
optimizing always takes a lot of code

그런데 이렇게 함수호출에 arguments를 인자로 전달하면, V8엔진이 해당 함수의 최적화를 스킵한다.
그래서 arguments를 사용할 때는 코드가 장황해지더라도 아래와 같이 최적화된 코드를 사용해야 한다.

😁 Array.from()

function foo(a, b, c) {
  console.log(Array.from(arguments)); // [1, 2, 3]
}

foo(1, 2, 3);  

😁 default parameter

function foo(...args) {
	console.log(args); // [1, 2, 3]
}

foo(1, 2, 3);

그래도 arguments 키워드를 못 놓겠다면
😁 arguments.length or arguments[i]를 사용하는 것이 좋다.

// goood
var args = new Array(arguments.length);
  
for (var i = 0; i < args.length; ++i) {
  args[i] = arguments[i];
}

//or
function anotherNotLeakingExample() {
  var i = arguments.length;
  var args = [];
                                
  while (i--) args[i] = arguments[i];
    
  return args
}
 

for...in

객체의 key에 접근할 때는 for...in 보다는 Object.keys(obj) 를 사용하는 것이 낫다.

delete

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

const obj1 = new Animal(1, 2);
const obj2 = new Animal(3, 4);

obj1.a = 30;
obj1.b = 100;
  
obj2.a = 30;
obj2.b = 100;

obj1과 obj2가 같은 Hidden Class를 공유하고 있는데 delete obj1.a 구문으로 key를 삭제해버린다면, obj1과 obj2는 같은 Hidden Class를 공유하게 되지 않게 되므로 최적화에 문제가 생긴다.

profile
매일 성장하는 프론트엔드 개발자

0개의 댓글