😎V8엔진은 어떻게..
https://evan-moon.github.io/2019/06/28/v8-analysis/
똑똑한 분의 포스팅을 읽고 대체 자바스크립트 코드는 어떻게 실행될 수 있는지가 너무 궁금해졌습니다.
물론 대부분의 이유는 저 포스트에서 잘 설명해주시고 있어서
저는 열심히 이해만 하면 되었습니다만..
역시 이해하는 것도 쉽지가 않네요
😎 간단하게 갑시다 간단하게
1. 자바스크립트 코드는 v8엔진에 의해 추상구문트리(AST)로 모두 변환된다.
(이과정에서 parser가 사용된다. v8엔진에서 파싱을 담당하는파일로는 parser.cc가 있다. )
2. 추상구문트리로 변환된 자바스크립트 코드는 v8엔진의 ignition interpreter가 한줄한줄 바이트코드로 변환한다.
이때 변환한 바이트코드는 재사용을 대비해 기억한다.
3. 이렇게 변환된 바이트코드는 v8엔진은 JIT(just in time compilation)
동적번역을 통해 바이트코드 -> 기계어로 변환하여 실행한다.
😎왜 굳이 컴파일 안하고 인터프리팅?
왜 굳이 컴파일하는게 아니라 인터프리팅하는가?
기존엔 전체 소스 코드를 한번에 컴파일하는 방식을 채택했지만
그렇게하니 컴파일 당시 메모리 점유가 너무 컸다.
또한 자바스크립트는 타입이 동적으로 마구 변할 수 있는 언어이기 때문에
기계어 수준에서도 타입이 동적으로 마구 변할 예외케이스에 대한 대비를 해야하다보니 코드가 너무 길어졌다.
😎--print-bytecode 옵션
function outer() {
return 1
}
Handler Table (size = 0)
Source Position Table (size = 217)
0x0306f7643e79 <ByteArray[217]>
[generated bytecode for function: get (0x03c9b8a0bf71 <SharedFunctionInfo get>)]
Bytecode length: 5
Parameter count 1
Register count 0
Frame size 0
Bytecode age: 0
4233 S> 00000306F7644406 @ 0 : 16 03 LdaCurrentContextSlot [3]
00000306F7644408 @ 2 : aa 00 ThrowReferenceErrorIfHole [0]
4249 S> 00000306F764440A @ 4 : a9 Return
Constant pool (size = 1)
00000306F76443B9: [FixedArray] in OldSpace
- map: 0x01a59a8012d9 <Map(FIXED_ARRAY_TYPE)>
- length: 1
0: 0x03e2dc9c26e9 <String[8]: #exitCode>
Handler Table (size = 0)
Source Position Table (size = 8)
0x0306f7644411 <ByteArray[8]>
node.js의 --print-bytecode 옵션을 통해
1을 반환하는 함수 outer 를 바이트 코드로 변환한 결과입니다.
그냥 터미널에서
node --print-bytecode hi.js
다음과 같이 실행시키고자하는 js 파일과 node 명령어 사이에
--print-bytecode 옵션을 주는것만으로도
저런 결과를 얻을 수 있다.
물론 뭔소린지는 이해가 되지 않는다.
다만 이상한점을 하나 눈치챌 수 있습니다.
분명 outer함수는 인자를 받지 않는데
parameter count는 1이 되어있는걸 알 수 있네요. 저 1은 this를 뜻한다고합니다.
😎내가 이해하기 힘들었던 것은
1. 우선 추상구문트리가 뭔지 모르겠다.
2. 바이트 코드는 뭐.. ok 실제로 보니까 저런거구나
3. JIT가 뭔데...?
😎추상구문트리..
function add(a, b) {
return a + b;
}
let result = add(2, 3);
console.log(result);
Program
└─FunctionDeclaration (name: "add")
├─Identifier (name: "a")
├─Identifier (name: "b")
└─BlockStatement
└─ReturnStatement
└─BinaryExpression (operator: "+")
├─Identifier (name: "a")
└─Identifier (name: "b")
└─VariableDeclaration (kind: "let")
└─VariableDeclarator
├─Identifier (name: "result")
└─CallExpression
├─Identifier (name: "add")
├─NumericLiteral (value: 2)
└─NumericLiteral (value: 3)
└─CallExpression
├─MemberExpression
│ ├─Identifier (name: "console")
│ └─Identifier (name: "log")
└─Identifier (name: "result")
다음은 챗지피티한테 물어봐서 얻은 추상구문트리의 예시이다.
중첩트리형태로 구성되어있고 각 코드들을 규칙에 맞게 분해해서 저장해놓는...느낌..?
뭐 대충 이해는 된다.
썡자바스크립트 코드를 바로 바이트 코드로 바꾸는건 힘드니까
우선 이런 형식으로 바꿔놓은 다음에 바꾼다는거구나
😎JIT(Just in time)
아까 봤던 내용을 설명해주는 것 같습니다.
특정 시점에 컴파일하여 최적화된 기계어 코드를 생성하고.
이 코드는 다시 실행될 때 빠르게 실행될 수 있다는 소리는 아마 재사용할 수 있다는 걸 말하는 것 같습니다.
자바스크립트 엔진은 JIT 컴파일러를 사용해서 실행 속도를 올리는데
먼저 파서로 추상구문트리로 변환해준 뒤
아까봤던 ignition 인터프리터로 바이트 코드로 변환한뒤 실행하고
실행 패턴을 추적해서 TurboFan JIT 컴파일러로 기계어 코드를 생성한다.
ok... 맨 위에 첨부한 포스팅에서도 다 나오는 내용이네요.
https://samslow.github.io/development/2020/07/06/JIT/
JIT에 대한 상세한 설명은 위 블로그를 참고했습니다.
그런데 보다보면 adaptive JITC에 대한 내용이 나옵니다..
적응형 JITC에서 엔진은 프로파일링과 런타임 분석의 조합을 사용하여
자주 실행되는 코드 경로를 식별한 다음 해당 경로에 공격적인 최적화 기술을 적용하여 성능을 향상시킵니다.
이는 일반적으로 실행 빈도에 관계없이 모든 코드 경로를 미리 컴파일하는 기존 JITC와 대조됩니다.
JavaScript에서 적응형 JITC의 이점은 가장 자주 실행되는 코드 경로를 선택적으로 최적화하는
동시에 덜 자주 사용되는 코드를 효율적으로 실행하여 프로그램의 전체 성능을 향상시킬 수 있다는 것입니다.
그 결과 페이지 로드 시간이 빨라지고 애니메이션이 부드러워지며 사용자 인터페이스 반응이 빨라집니다. JavaScript의 적응형 JITC 엔진에서 사용하는 일부 기술에는 개체 속성을 빠르고
효율적으로 조회할 수 있는 인라인 캐싱과 엔진이 프로파일링 데이터를 기반으로
프로그램의 동작에 대한 가정을 한 다음 그에 따라 최적화할 수 있는 예측 최적화가 포함됩니다. .
전반적으로 적응형 JITC는 최신 JavaScript 엔진에서 고성능을 달성하기 위한 중요한 기술이며
웹 개발 분야에서 지속적으로 연구 개발되고 있는 영역입니다.
대충 JITC를 더 효율적으로 최적화해주는 기술을 적용한 녀석이라는 것이네요
😎 자바스크립트 컴파일의 역사
다음 PPT 내용은
위 링크에서 참고한 내용입니다.
https://v8.dev/blog/launching-ignition-and-turbofan
먼저 2010년도의 컴파일 파이프라인입니다..
Full-Codegen이 아주 눈에 띈다. 아까봤던 기존의 컴파일 방식은
너무 많은 메모리를 사용한다는 문제점을 가진다했을 때 그 기존의 컴파일 방식을 이야기하는 듯합니다.
저 Full-codegen 방식을
ignition과 TurboFan으로 대체한다는 내용을 담은거구나.
unoptimized 는 최적화되지않은이라는 뜻입니다..
optimize는 최적화하다.
deoptimize는 최적화를 해제하다.라는 뜻.
그러니까 Full-codegen은 자바스크립트 코드를 읽어보고
최적화가 필요한(재사용이 많이되는 코드)는 CrankShaft에게 보내어 최적화를 시키고
최적화가 필요없는 unoptimized한 코드는 그냥 사용을 하고.
또 optimize해서 사용하다가 더이상 자주 사용되지 않는 코드는
deoptimize해서 최적화를 해제해주고...
이제 crankshaft만 알면 저 그림들이 대략적으로 이해가 될 것 같습니다.
😎crankShaft
CrankShaft는 최적화를 담당하는 녀석이었다는 거 였습니다.
즉 v8엔진의 변화는 다음과 같다.
Full-Codegen -> ignition interpreter
CrankShaft -> TurboFan
코드를 컴파일하는 것과 최적화를 해야한다는 목적은 남았으나
그 목적을 구현하기 위한 방법이 바뀐것이라고 이해할 수 있을 듯 합니다..
그러니까 다시 저 그림을 보면
Full-codegen이 모든 코드를 컴파일해서 CrankShaft에게 넘겨주면
CrankShaft는 최적화된 코드를 뱉어준다...라는 걸까?
crankShaft의 최적화기술은 인라이닝, 타입추론, 분기최적화, 루프최적화가있다고합니다..
위키백과의 v8엔진에 대한 내용과 대조해본 결과
인라이닝은 인라인 함수를 말하는 것 같고
crankShaft역시 앞서 살펴본 adaptive JITC의 일종이라는 것 같네요.
https://namu.wiki/w/%EC%9D%B8%EB%9D%BC%EC%9D%B8%20%ED%95%A8%EC%88%98
무려 나무위키까지 개설되어있는 인라인 함수
나는 처음 들어봤다.
😎 인라인 함수
C/C++ , Kotlin에서 사용할 수 있는 기능이라고 합니다.
자바스크립트에는 없는 기능...
하지만 우리가 명시적으로 쓸 수 없다는 거지
v8엔진은 알아서 인라이닝을 사용한다는 것으로 유추할 수 있을 것 같습니다..
인라인 함수를 한줄로 요약하면 다음과 같다.
함수 호출을 하는 대신 그 함수의 내용물을 집어넣어준다는 것입니다.
그럼 인라인 함수를 왜 써야하는걸까?
킹무위키에 따르면 CPU 파이프라인의 구조상 함수 호출은 꽤 비용이 비싼 작업이다.
따라서 1. 비싼 함수 호출을 최대한 덜하기 위해서 호출코스트를 줄여주기 위해
2. 인라인 이후 추가적으로 이루어지는 최적화가 가능해진다.
솔직히 말해서 2번은 전혀 이해가 안됩니다.
검색해보니 함수 호출을 하는 경우엔 컴파일러가 함수의 결과값을 알 수 없기때문에
최적화가 불가능하던걸..(모르는데 어떻게 해요)
인라인을 통해 호출코드가 그 함수의 코드로 대체되면
추가적인 최적화가 가능해진다는 것(실행에 필요한 로직만 남겨두는 최적화..?)같네요
그런데 또 너무 복잡한 함수를 인라이닝 하면 안좋다고 한다...
코드가 너무 비대해지기 때문에..
지피티한테 물어보니 이런 답변을 줬습니다.
인라인 함수는 별도의 함수 호출로 실행되는 것이 아니라 호출되는 시점에서 인라인으로 확장되는 함수입니다.
즉, 인라인 함수가 호출되면 함수 내부의 코드가 호출 코드에 직접 복사되고
호출 코드의 일부인 것처럼 그곳에서 실행됩니다.
인라인 함수는 일반적으로 구현이 간단한 작고 자주 사용되는 함수에 사용되며
함수 호출 오버헤드의 오버헤드를 피하기 위해 성능이 중요한 코드에서 자주 사용됩니다.
예를 들어 다음 C++ 코드를 고려하십시오.
inline int square(int x) {
return x * x;
}
int main() {
int a = 5;
int b = square(a);
return 0;
}
위 C++ 코드는 인라이닝에 의해
int main() {
int a = 5;
int b = a * a;
return 0;
}
이렇게 치환된다.뭐... ok
그냥 함수 호출을 그 함수 내용으로 덮어 씌워버린다.
같은 개념이라는 거군요
근데 저렇게 한줄이면 직관적으로 이해되는데 엄청 긴 함수면..?
상상이 잘 안되긴 하네요
inline의 정확한 의미는 컴파일러에게
“여러 translation unit에서 중복으로 정의되었더라도 모두 동일한 것이므로 그 중에서 아무거나 사용해도 좋다“
고 친절하게 알려주는 것이다.
https://othereum.github.io/c/c++/inline/
위 블로그에서 한줄 요약을 가져왔습니다.
역시 세상엔 똑똑한 사람이 많군요..
다만 저건 C++의 문법 inline 키워드의 정확한 의미를 알려주는 것이니
지금 애초에 인라인이 뭔지도 모르는 상황에서 무슨 의미가 있는데? 싶을 수 있지만..
아무튼 도움됨
뭔가 하나로 이어질 것 같은 느낌인데
crankshaft는 이 최적화 기법을 통해 함수 오버헤드를 피한다... 근데 저는 오버헤드가 뭔지도 몰라요
😎오버헤드 오버헤드.. 안좋은건 알겠는데 이게 뭐임
내가 아는 오버헤드는 오버헤드 킥 뿐이다.
그러니 지피티한테 물어보도록하자..
영어로 물어보는게 퀄리티나 속도 전부 훌륭하기때문에
영어로 물어보고 번역기를 돌렸다.
프로그래밍에서 "오버헤드"는 작업을 수행하는 데 꼭 필요한 것 이상으로
프로그램에서 소비하는 추가 리소스(예: 시간, 메모리 또는 처리 능력)를 나타냅니다.
다음은 위키백과의 설명이다.
예를 들어 A라는 처리를 단순하게 실행한다면 10초 걸리는데,
안전성을 고려하고 부가적인 B라는 처리를 추가한 결과 처리시간이 15초 걸렸다면,
오버헤드는 5초가 된다. 또한 이 처리 B를 개선해 B'라는 처리를 한 결과,
처리시간이 12초가 되었다면, 이 경우 오버헤드가 3초 단축되었다고 말한다
흠.. 그러니까 꼭 필요한 시간,메모리보다 더 소비되는 시간,메모리를 오버헤드라고 하는 거군요
이건 좀 더 직관적이네요 ok...
😎 지금은 어케 컴파일함?
2017년으로 왔습니다.
Full-codegen과 crankshaft는 레거시로 사라지고
그 자리를 ignition과 TurboFan이 대체하는 모습이네요
그 와중에 fullcodegen, turbofan, crankshaft를 모두 섞어쓰는
끔찍한 혼종 시기가 있었네요..
https://www.youtube.com/watch?v=r5OWCtuKiAk
위 링크에서 풀 영상을 시청하실 수 있습니다.
그러다가도 썸네일에서는 거기에 ignition까지 추가되는... 정말 끔찍한 혼종이;;
너무 깊게 들어가면 정신을 잃어버릴 것 같으니 여기서 멈추겠습니다.
😎 인라인 캐싱과 히든 클래스
https://engineering.linecorp.com/ko/blog/v8-hidden-class
저 둘은 TurboFan에서 사용하는 최적화 기법이다.
인라인 캐싱은 위에서 살펴봤고 히든 클래스는... 위 포스트를 참고해보도록하자.
자바스크립트는 동적으로 타이핑 되는 언어이고
그러다보니 정적 타이핑 언어에 비해 객체의 프로퍼티에 접근하는 속도가 떨어질 수 밖에 없다.
바로 정해진 곳으로 찾아가는것과 코드의 문맥을 해석하고 난 다음 찾아가는 것은...ㅎㅎ;;
그런데 이 동적 탐색을 회피하기 위해서 자바스크립트는 히든 클래스라는 기법을 사용한다는 것이다.
히든 클래스의 특징은 다음과 같다.
어... 그래요... 대충 프로토타입이랑 비슷해보이는데
객체는 반드시 하나의 히든 클래스를 참조한다. 히든 클래스는 각 프로퍼티에 대해 메모리 오프셋을 가지고 있다. 동적으로 새로운 프로퍼티가 만들어질 때, 혹은 기존 프로퍼티가 삭제되거나 기존 프로퍼티의 데이터 타입이 바뀔 때는 신규 히든 클래스가 생성되며, 신규 히든 클래스는 기존 프로퍼티에 대한 정보를 유지하면서 추가적으로 새 프로퍼티의 오프셋을 가지게 된다. 히든 클래스는 프로퍼티에 대해 변경이 발생했을 때 참조해야 하는 히든 클래스에 대한 정보를 갖는다. 객체에 새로운 프로퍼티가 만들어지면, 현재 참조하고 있는 히든 클래스의 전환 정보를 확인한 후, 현재 프로퍼티에 대한 변경이 전환 정보의 조건과 일치하면, 객체의 참조 히든 클래스를 조건에 명시된 히든 클래스로 변경시킨다.
눈에 띄는건 기존 프로퍼티가 삭제되거나 데이터타입이 바뀔 때 신규 히든 클래스가 생성된다.
원숭이식으로 생각해보자면 데이터타입은 안 바꿔주는게 성능에 더 좋겠군요..?
솔직히 이제 뇌가 과부하가 와서 더 못찾아보겠습니다.
😎 요약
자바스크립트는 내 코드를 v8엔진의 ignition과 TurboFan을 이용해 열심히 기계어로 컴파일 해주고있다.
고맙다 최고 ignition 최고 TurboFan아!!
'javascript' 카테고리의 다른 글
getter,setter까지 복사하는 deep copy를 구현하자 (0) | 2023.03.02 |
---|---|
프로토타입을 케이크처럼 쉽게 먹는 방법 (3) | 2023.02.28 |
자바스크립트의 모듈을 내가 만들어서 써보자! (1) | 2023.02.24 |
DOM을 다루는 실전적인 방법 (0) | 2023.02.14 |
두 배열이 동등한지 비교하는 방법 (0) | 2023.01.30 |