개발/javascript

자바스크립트의 작동 원리(V8 Engine)

ebang 2023. 11. 2. 23:00
반응형

#개발 #javascript  번역 (사이트1 사이트2)

JavaScript는 Google의 V8엔진을 사용한다.

또한 싱글 스레드임과 동시에 콜백큐를 사용한다.이에 대해 자세히 알아보자.

The Javascript Engine

- Javascript engine은 프로그램 혹은 인터프리터로, 자바스크립트 코드를 실행하는 역할을 한다.

 

 

 

 

자바스크립트 엔진은 표준 인터프리터로 구현될 수도 있고, 혹은 자바스크립트를 단순히 bytecode로 컴파일 하는 컴파일러일 수도 있다.

자바스크립트 엔진을 구현하는 유명한 프로젝트에는 다음과 같은 것들이 있다.

  • V8 : 오픈소스 , 구글이 만들었고 c++로 구현되어있다.
  • Rhino : 오픈소스, Mozilla 기업이 만들었다. Java로 구현되어있다.
  • SpiderMonkey : 첫번째 자바스크립트 엔진. Firefox에서 사용된다.
  • JavaSCriptCore : 오픈소스, Apple이 사파리를 위해서 구현했다. Nitro 라는 이름으로 알려져 있다.
  • KJS : KDE의 엔진으로, KDE 프로젝트를 위해서 Harri Porten이 원조로 개발했다.
  • Chakra(JScript9 지원) : Internet Explorer
  • Chakra (Javascript 지원) : Microsoft Edge
  • Nashorn: OpenJDK의 일부로 오픈소스, Oracle Java 언어와 Tool Group 으로 개발되었다.
  • JerryScript : 사물 인터넷을 위한 경량 엔진

V8엔진

JavaScript의 유명한 엔진은 Google의 V8 엔진이다.

V8 엔진은 Chrome, Node.js 에서 사용되는 것으로도 유명하다.

V8 엔진은 왜 생겼을까?

처음에 V8엔진은 웹 브라우저 내에서 JavaScript 실행 시 성능을 끌어올리기 위해 고안되었다.

속도를 올리기 위해, V8 엔진은 인터프리터를 사용하는 대신 자바스크립트 언어를 더 효율적인 기계어로 바꾼다. 어떻게 하냐면, 자바스크립트 코드를 실행 시에, JIT (Just-In-Time) 컴파일러로 기계어로 변환하는데, bytecode나 중단 단계의 코드 변환 없이 컴파일한다.

 

 

 

  • 컴파일러

5.9 버전 이전에, 엔진은 상대적으로 느린 기계어를 만드는

full-codegen 엔진이랑 최적화 컴파일러인 Crankshaft 컴파일러를 갖고 있었다. (현재는 사라짐)

  • 스레드
    • 코드를 fetch, 컴파일, 실행하는 메인 쓰레드
    • 컴파일을 위한 여러 스레드 (코드 최적화 하는 동안 메인스레드는 실제로 실행할 수 있다.)
    • 런타임에게 어떤 메소드가 오래 걸릴 것 같은 지 알려줘서 Crankshaft가 최적화할 수 있도록 하는 프로파일러 스레드
    • Garbage Collector Sweep을 관리하는 스레드
    •  
  • 두 컴파일러의 사용

처음에 자바스크립트 코드를 실행하면, V8 엔진은 Full-codegen을 사용하여 파싱된 자바스크립트를 곧바로 기계어로 번역한다.(중단간계 bytecode 없이.) 그래서 매우 빠르게 실행할 수 있도록 한다.

 

코드가 실행되는 중에, 프로파일러 스레드가 최적화할만한 메소드들에 대해 정보를 취합한다.그 다음 Crankshaft 최적화가 다른 스레드에서 시작된다. Javascript 의 추상 Syntax 트리를 고수준의 static single-assignment (SSA), 다른 말로 Hydrogen으로 변환한다음 Hydrogen graph를 최적화하려고 시도한다.

대부분의 최적화는 이 단계에서 일어난다.

  • inlining
    첫번째 최적화는 최대한 코드를 할 수 있는 한 inlining 하는 것이다.
    함수를 실행하는 부분을 실제 함수 내용으로 바꾸는 것이 inlining이다.
  • Hidden class
    Javascript 는 prototype-based 언어이다: 클래스가 없고, 객체들은 cloning process를 이용해서 만들어진다. javascript는 또한 동적 프로그래밍 언어이기 때문에 한 객체의 속성이 초기화 이후에 쉽게 추가되거나 삭제될 수 있다.

 

Hidden Class 

자바와 같은 언어들은 객체를 정의하는 순간(Class) 그 속성에 대한 정보 또한 변경되지 않는다. 

즉 컴파일 시점 전에 객체의 속성이 모두 정해지고 고정되며, 런타임 시점에서 추가되거나 삭제되는 일이 일어날 수 없다. 

이와 다르게 javascript는 변동이 가능하므로, 이를 더 효율적으로 다루는 과정이 필요하다. 

(속성에 접근하기 위해서 매번 탐색하는 과정이 필요하기 때문.)

이를 위해서 Hidden Class 라는 개념이 등장했다. 

 

쉽게 말하면 같은 클래스이더라도 어떤 속성을 갖고 있느냐에 따라서 Hidden Class가 업데이트된다. 

Hidden Class는 자신의 위치로부터 특정 속성을 접근하기 위한 거리가 얼마나 인지 offset을 통해서 저장하기 때문에, 속성에 접근하기가 쉬워진다. 

 

예시를 보자. 

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

var p1 = new Point(1, 2);

new Point(1,2) 를 만든 순간, V8은 C0라고 불리는 hidden class를 만든다.

속성이 정의된 것은 없기 때문에, C0는 아직 비어있다. Point 함수 내에서 this.x = x 문을 실행하게 되면, V8은 C0 기반의 C1이라는 hidden class를 만든다.C1 클래스는 x라는 속성의 값을 불러올 수 있는 위치를 저장한다.  그리고 이 위치는 offset으로 표시된다.이와 동시에 C0 클래스는 class transition을 추가하여 'x' 속성이 추가되면 C1 class 로 변환될 것을 알리는 안내를 저장한다. Point 객체를 가리키는 hidden class는 C1이 된다.

비슷하게 C1 상태에서 this.y = y 문이 실행되면, C1 class에서 C2 class가 생기고, x,y 값이 저장된 주소에 대한 offset이 저장된다. Point 객체는 C2 Hidden class가 가리키게 된다. 




inline caching

V8이 javascript 코드를 최적화하는 방법 중 하나이다. 

특정 함수는 특정 객체가 실행하는 것이 반복적으로 일어난 다는 것을 가정으로 삼아, 이를 캐싱하는 것이다.

이전에 설명된 hidden class와 관련이 있는데, hidden class에서 특정 메소드가 2번 이상 호출되면, 그 위치를 객체 자체에 저장해두고 바로 접근해서 메소드를 사용하는 원리이다.  (원래는 hidden class에 정의되어있을 것이고 접근하는데 탐색이 필요했을 것)  

더 심화해서 얘기할 때 나오는 얘기지만, 이게 통하려면 객체들이 같은 hidden class를 공유해야 한다. 위의 Point 객체가 x, y를 갖고 있다고 할 때, x,y 순으로 초기화하느냐, y,x순으로 초기화하느냐에 따라 hidden Class는 두 종류가 생길 수 있는데, 이 순서를 같게 해두어야 같은 hidden Class 라는 것이 보장이 되고 inline caching 을 사용하는 것이 의미가 있어진다. 

 

보통 컨벤션에 있는 초기화 순서 같은 것도 최적화에 영향을 미친 다는 것이 신기한 점이었다.

 

 

초기화 순서 때문에 서로 다른 hidden Class가 되어버린 두 객체.




기계어로 컴파일 (Compilation to machine code)

위의 과정을 통해 최적화가 일어나고 나면, 기계어로 컴파일 된다. 



safeguard

engine은 최적화를 한 것이 실제로 최적화되지 않았다든지, 가정한 것이 옳지않았을 것에 대비해 다시 되돌릴 수 있는 deoptimization 이라 불리는 안전 가드도 있다. 


Garbage collection

garbage collection을 위해서 V8은 전통적인 접근법인 mark-and-sweep 기법을 사용한다.marking 단계에서는 Javascript 실행을 멈춘다. GC 비용을 관리하고 실행을 더 안정하게 하기 위해 V8은  '증분' marking을 쓴다:  즉 모두 탐색하면서 sweep 하는 것이 아니라, 

일부만을 탐색하고 실행하고, 그 다음 탐색에서는 이전에 탐색했던 부분 다음부터 탐색하는 것이다. 

이를 통해 짧은 시간동안만 멈추었다가 재가동할 수 있다.

 

Ignition and TurboFan

5.9 버전 이후로  실제 자바스크립트 애플리케이션에서 성능 개선과 메모리 절약이 이루어졌다.

새로운 실행 파이프라인은 V8의 인터프리터인 Ignition과 TurboFan 위에 만들어졌고 

*5.9 버전이 나온 후로, full-codegen, Crankshaft 기술은 이제 안쓰인다고 한다.

 

 *V8이 예전보다 전반적으로 더 성능이 좋고 유지하기 좋은 아키텍처를 갖고 있다는 뜻이 되겠다.

 

벤치마킹




최적화된 자바스크립트 코드를 작성하는 방법

위의 엔진을 바탕으로 최적화된 자바 스크립트 코드를 작성하는 방법도 소개되어있길래 가져와봤다.

 

 

1. 객체 속성의 순서 : hidden class와 최적화된 코드가 공유될 수 있게 하기 위해서 객체의 속성값 초기화는 모두 같은 순으로 한다. 

 

2. 동적 요소 : 객체의 초기화 이후에 속성을 추가하는 것은 hidden class 가 이미 최적화된 상태에서 변화하는 것이기 때문에 성능 저하를 야기할 수 있다. 따라서 생성자에서 객체의 속성을 정의하는 것이 좋다.

 

3. 메소드 : 같은 메소드를 호출하는 코드는 서로 다른 메소드를 호출하는 코드보다 더 빠르게 수행될 것이다. (inline caching으로 인해)

 

4. 배열 : key가 증가하는 숫자가 아닌 sparse array는 피하라.  안에 요소가 없는 sparse 배열은 hash table이다. 그런 배열에 접근하는 것은 비용이 더 많이 든다.  마찬가지로 배열의 요소를 삭제하지 말라. 이건 key sparse를 만든다. 또한, 미리 큰 배열을 할당해놓는 것도 좋지 않다.

 

5. tagged 값 : V8은 객체와 숫자들을 32 비트로 표현한다. 그것이 객체인지(flag = 1), 정수인지(flag =0) 나타내기 위해 SMI라고 부르는 1 비트를 사용한다. 그리고, 숫자의 값이 31비트보다 크다면 숫자를 double로 만든 후 숫자를 저장하기 위한 새로운 객체를 만든다. 따라서 이러한 boxing operation을 줄이기 위해 31 bit signed number를 사용하는 것이 좋다. 

 

반응형

'개발 > javascript' 카테고리의 다른 글

javascript의 .map 파일  (0) 2023.11.19