5개의 글 중 2개가 살아남았는데 한번 알아보자.
December 15, 2014
컴파일러는 정말 멋지다, 그렇지 않은가? 모든 프로그래밍 개념은 언젠가는 컴파일러 구현에 사용될 가능성이 있다. V8 버그 트라이지이팅이나 무작위 코드 탐색 중에 항상 발견되는 것들에 항상 놀라움을 느낀다.
V8에서 항상 열정적으로 관심을 가지고 있었지만 실제로 제대로 이해하지 못했던 것은 Deoptimize이다. 여기에서의 아이디어는 V8은 코드를 최적화하여 더 빠르게 실행하도록 만드는데, 이 최적화는 타입, 범위, 실제 값, 상수 등에 대한 가정에 의존한다. 이러한 가정은 조건이 충족되지 않을 때 최적화된 코드가 실행되지 않을 것을 의미한다. 가정이 실패할 때 이전의 "가정 없는" 버전의 생성된 코드로 돌아가면서 최적화된 코드를 "Deoptimize"해야 한다는 것을 의미한다.
기술적으로 말하면, 이는 컴파일러가 사실상 두 개의 컴파일러인 베이스 컴파일러와 "Optimize"로 구성되어 있다는 것을 의미한다. (JSC나 SpiderMonkey를 얘기하면 더 많은 컴파일러가 될 수도 있다.) 이 개념은 꽤 탄탄하며 놀라운 성능을 발휘할 수 있지만 한 가지 주의할 점이 있다. 최적화된 코드는 진입점뿐만 아니라 여러 곳에서 "Deoptimize"될 수 있으므로 환경(로컬 변수, 인수, 컨텍스트)을 매핑하고 이동해야 한다.
더 잘 이해하기 위해 해야 할 일과 일이 어떻게 진행되는지 알아보기 위해 프로그램을 JIT 컴파일하는 대신 해석하는데 사용할 수 있는 기본적인 스택 머신을 고려해보자.
참고로 이 스택 머신과 하단의 어셈블리는 추상 컴파일러의 출력물이며 v8과는 아무 상관이 없다. 따라서 여기서는 단지 설명을 위한 것이다.
push a
push b
push c
mul ; pop 2 values and push `arg0 * arg1`
push d
mul ; b * c * d
add ; pop 2 values and push `arg0 + arg`
ret ; pop and return value
해석기는 한 번에 한 명령을 실행하며, 각 지점에서 스택을 유지한다.
이제 x86_64와 같은 레지스터 머신을 상상해보고 어셈블리어로 동일한 프로그램을 작성해보자. 조금 더 흥미롭게 만들기 위해 대상 아키텍처가 레지스터를 두 개만 가지고 나머지 값은 메모리(스택)에 저장되어야 한다고 가정해보자.
mov [slot0], a ; store value in 0 memory slot
mov rax, b ; store value in rax register
mov rbx, c ; store value in rbx register
mul rax, rbx ; rax = rax * rbx
mov rbx, d
mul rax, rbx ; rax = b * c * d
mov rbx, [slot0] ; load value from 0 memory slot
add rax, rbx ; rax = b * c * d + a
명령은 하나씩 실행되며 레지스터 값과 메모리 슬롯을 유지한다.
우리 컴파일러의 관점에서 첫 번째 코드는 프로그램의 최적화되지 않은 버전이고, 두 번째 코드는 최적화된 버전이다. 사실, 이는 x86_64 플랫폼에서 실행할 경우 완전히 유효한 주장이다. 어셈블리는 에뮬레이션해야 하는 해석 코드보다 실행 속도가 훨씬 빠르다.
어셈블리에서 두 번째 mul
명령은 d가 작은 정수일 때만 작동한다고 가정해보자 (d는 rbx
레지스터에 저장되어 있음). 이제 실행이 mul
에 도달하고 JavaScript 문자열이 있는 것을 발견하면 "올바른 작업"을 수행하지 못할 것이다. 이 mul
(num
, str
) 작업은 어떤 형식의 강제 변환이 필요하며, 해석기에서는 쉽게 처리할 수 있다. 어셈블리에서 이 작업을 수행하는 것은 성능 면에서 훨씬 더 비용이 많이 들 수 있다. 이를 처리하기 위해 컴파일러는 체크 명령을 삽입한다.
mov [slot0], a
mov rax, b
mov rbx, c
checkSmallInt rax
checkSmallInt rbx
mul rax, rbx
mov rbx, d
checkSmallInt rax
checkSmallInt rbx
mul rax, rbx ;
mov rbx, [slot0]
add rax, rbx
이와 같이, mul의 인수가 작은 정수가 아닌 경우에는 이 코드를 어떤 방식으로든 어셈블리 코드에서 스택 기계로 "비최적화"하여 해석 버전에서 계속 실행해야 한다. 여기서 비최적화가 시작되는 최적화된 코드의 위치이다.
mov [slot0], a
mov rax, b
mov rbx, c
checkSmallInt rax
checkSmallInt rbx
mul rax, rbx
mov rbx, d
checkSmallInt rax
checkSmallInt rbx <-----
mul rax, rbx ;
mov rbx, [slot0]
add rax, rbx
...그리고 계속하고자 하는 비최적화된 코드의 위치는 어디일까?