TIL] V8!!!V8!!!V8!!!

Song-Minhyung·2023년 7월 13일
0

TIL

목록 보기
4/12

컴파일러

컴파일러는 여러 단계를 거쳐 결과물을 만들어낸다.
그런데 이 때 처음에 공통적으로 하는 작업이 있는데 바로 파싱이다.
이 때 어휘 분석과 구문 분석을 거치는데 어휘분석은 tokenizer, lexer로 나뉜다.
https://velog.io/@song961003/TIL-regexp-ast 여기서 구현한게 어휘분석기 ,구문분석기다.

이렇게 만들어진 구문(Tree)을 가지고서 c 컴파일러는 아래의 과정을 거친다.
의미론적 분석을하고 -> 어셈블리어로 변환후 ->
기계어로 바꾸고 -> 목적파일을 만들고 ->
링크를 해서 -> 바이너리 파일이 생성된다.

메모리 구조

출처: https://medium.com/@jungkim/스위프트-타입별-메모리-분석-실험

이번 시뮬레이터 에서는 실제 전체 메모리 구조를 구현하지는 않았고 Stack, Heap, Text만 구현했다.

Stack은 함수를 호출 할 때마다 지역변수, 매개변수 리턴값 등이 쌓이고
Heap은 동적으로 할당된 메모리가 들어간다.
Text는 어셈블리 명령어 한개씩이 들어가게 된다.

그리고 Stack은 높은 메모리 주소에 저장되고 Text는 그 반대다.

V8엔진

출처: https://deepu.tech/memory-management-in-v8/

V8 엔진에서 관리하는 메모리를 조금 더 자세히 살펴보면 힙 메모리는 가비지 컬렉션을 수행하는데 모든 영역에서 수행하는게 아니라 New, Old 영역에 대해서만 수행한다.

여기서 New는 새로 만든 객체, Old는 GC가 2번 발생할동안 살아남은 객체를 저장해준다.
Old는 또 두개 영역으로 나뉘는데 pointer는 다른 객체를 참조하는 애들이 모여있고,
data에는 값이 들어있는 애들이 모여있따.

그 외 영역은 아래와 같다.

  • 라지 오브젝트 영역: 다른 영역의 제한된 크기보다 큰 객체들이 모여있는곳, GC도 안통함.
  • 코드 영역: 컴파일러가 컴파일된 코드를 저장하는곳. 유일하게 실행 가능한 메모리가 모여있다.
  • 셀 영역, 속성 셀 영역, 맵 영역: 각각 Cells, PropertyCells, Maps을 포함하고, 모두 같은 크기의 객체를 참조하는지 제약이 있어 수집을 단순하게 만든다.

그리고 스택은 메서드와 함수 프레임, 원시 값, 객체 포인터를 포함한 정적 데이터가 저장된다.
참고로 함수프레임(FP)는 함수의 시작위치가 들어있는 레지스터다. 그 레지스터가 갖고있던 값을 넣는다.

V8에서 heap과 stack

여기 링크를 살펴보면 js 코드를 읽을 때 stack, heap 메모리에 어떤 일이 일어나는지 볼 수 있다.

  • 우선 전역스코프는 Global Frame안에 기록되고, 그 값은 heap memory를 가리킨다.

    출처: https://speakerdeck.com/deepu105/v8-memory-usage-stack-and-heap

  • 그리고 함수가 추가로 호출되면 스택에 쌓이고 종료되면 스택이 없어지며, return값이 남는다.
    이 부분은 fp(프레임 포인터)와 pc(프로그램 카운터)로 구현할 수 있다.
    이렇게 구현하는 방법은 이곳에 정말 자세히 설명되어 있어서 개념을 익히는데 큰 도움이 되었다.
    • 함수를 호출할 때
      1. 우선 함수가 호출되면 스택에 pc, fp를 순서대로 push한다 ( 나중에 돌아오기 위해)
      2. 그 후 fp에 sp 기록한다 ( 함수 종료 후 돌아올 위치 기록 )
      3. 마지막으로 pc에 다음 함수 시작위치를 기록한다. 그러면 그곳부터 실행된다.
    • 함수를 종료할 때
      1. sp에 fp를 기록한다 ( sp를 함수 호출 전으로 옮김 )
      2. pop()
      3. fp에 sp의 값을 기록한다 ( fp를 현재 함수 스코프 시작위치로 옮김 )
      4. pop()
      5. pc에 sp의 값을 기록한다. ( pc를 다음 코드 실행위치로 옮김 )
      6. result 값이 있다면 그 값을 EAX에 넣고 해당 주소를 스택에 PUSH한다
      7. 이번 명령어가 SET이라면 pop하여 해당 값을 할당해준다.

실행 컨텍스트 원리?

우선 실행 컨텍스트 ( Execution Context ) 은 실제로 존재하는건 아니고 ECMA Script의 스펙에서 설명하는 추상적인 개념이다. 그래서 실제로 접근할수는 없다.
예전에 이곳에 공부한 내용을 자세히 써놨으니 생각이 안나면 다시 가서 보자!

간단히 다시 적으면 컨텍스트는 자바스크립트 코드가 실행되는 환경을 의미한다.
실행 컨텍스트는 실행 가능한 코드가 실행될 때마다 생성되며, 변수 객체(variable object), 스코프 체인(scope chain), this 바인딩 등의 정보를 포함한다

실행 컨텍스트 스택 ? 콜 스택 ?

실행 컨텍스트 에서는 해당 스택 프레임의 정보들(variable objectscope chain, this 바인딩 등의 정보)가 들어가 있다고 생각한다. 그리고 콜 스택에는 이제 실제로 지역 변수들, 함수들 등이 들어가 있다고 생각한다.

js는 실행 환경을 따로 저장하므로 아마도 이 둘을 따로 관리하는것처럼 보인다.
이 둘은 정말 비슷한 개념이고 같다고 생각해도 될것같은데 그래서 콜 스텍은 실행스텍이라 부르는 사람도 존재하는것같다 -> 참조

가비지 컬렉터

위에서 힙 메모리에 할당된 값들은 해제를 해주지 않으면 계속 남아있는다.
그래서 V8에선 마이너 GC, 메이저 GC를 돌리며 이 쓰레기 값들의 할당을 해제해준다.

마이너 GC

우선 이곳에 순서가 자세히 나와있다.
위에서 힙의 New 공간에서 돌아가는게 마이너 GC인데 간략한 동작 과정은 아래와 같고 to from 영역이 있다.

출처: https://speakerdeck.com/deepu105/v8-minor-gc

  1. from 영역에 메모리에 값을 할당하려 한다. 만약, 꽉차있면 마이너 GC가 실행된다.

  2. GC루트(스택 포인터)에서 시작해 from 영역의 객체 그래프를 재귀적으로 탐색하며 현재 사용중인 객체들만 to 영역으로 옮기고, 옮겨진 객체를 가리키던 포인터는 갱신된다. 이 과정이 끝나면 to 공간을 압축해 메모리 단편화를 줄인다.(아래이미지)

    출처: https://speakerdeck.com/deepu105/v8-minor-gc

  3. to로 못옮겨지고 from에 남은 애들은 가비지로 취급돼 가비지 컬렉트 된다.

  4. to와 from 공간에 있는 객체들을 맞바꿔 to로 옮겨진 객체들은 다시 from에 존재하고, to는 비어있게 됨. (아래이미지)

    출처: https://speakerdeck.com/deepu105/v8-minor-gc

  5. 시간이 흘러 새로운 객체를 from에 할당하려 하는데 공간이 없다면 마이너 GC를 실행함.(아래이미지)

    출처: https://speakerdeck.com/deepu105/v8-minor-gc

  6. 2~4가 반복된뒤 살아남은 객체는 2번 살아남았기에 Old 공간으로 옮겨지게됨.(아래이미지)

    출처: https://speakerdeck.com/deepu105/v8-minor-gc

  7. to와 from 공간에 있는 객체들을 맞바꿔준후 새로운 객체를 from에 할당(아래이미지)

    출처: https://speakerdeck.com/deepu105/v8-minor-gc

메이저 GC

메이저 GC는 Old 영역에서 사용하지 않는 메모리들의 할당을 해제해고, V8에서 Old 영역의 메모리가 모자르다고 판단할 때 발생한다.
그리고 Old 영역은 마이너 GC 주기가 실행된 후 2번 살아남은 객체들로 채워지게 된다. 근데 마이너 GC의 작동방식은 영역 내의 모든 부분을 탐색하기에 메모리 영역이 크면 정말 비효율적으로 작동할것이다.

그래서 New 영역은 Mars-Seep-Compact 알고리즘을 사용하는 메이저 GC를 사용해 힙 메모리에서 참조되지 않고 있는 메모리들의 할당을 해제해준다.
이 알고리즘은 Tri-color(흰색-회색-검은색) 마킹 시스템을 사용하고 세 단계의 프로세스를 거친다. 그 후 변경된 메모리들의 포인터를 갱신해준다.

  1. 마킹: 마이너 GC처럼 이 알고리즘도 어떤 객체가 사용중인지 제일 처음에 식별한다.
    이때, 사용중이거나 GC 루트(스택 포인터)에 재귀적으로 도달할 수 있는 객체들을 활성 상태로 표시된다.
    작동방식은 dfs 알고리즘으로 도달할 수 있는곳을 사용중이라 판단하고 마킹해준다.
  2. 스위핑: 힙 메모리를 순회하며 마킹 단계에서 체크되지 않은 메모리 주소들을 기록한다.
    이 공간은 사용 가능한 목록(free-list)에서 사용 가능하다고 표시되며 다른 객체들을 저장할 수 있게된다.
  3. 압축: 스위핑 단계 이후 필요하다 판단되면 활성 상태인 객체들을 한곳으로 모아 단편화를 방지하기위해 메모리 구조를 재조정 한다. 이 단계에서는 상대적인 위치를 변경하거나, 불필요한 간격을 제거하는 방식으로 수행된다. 이렇게 연속적인 메모리에 객체들이 모임으로 인해 메모리 할당을 효율적으로 할 수 있게된다.

Orinoco

그런데 위 두개의 GC는 작동할 때 프로그램이 멈춘다. 이를 stop-the-word라고 한다.
이 시간이 길면 길어질수록 페이지가 느려지거나 렌더링 시간이 길어질 것이다.
이를 피하기 위해서 V8에서는 아래의 기술을 사용한다.

Parallel

원래 메인 쓰레드 하나가 혼자 일을 하던걸 헬퍼 쓰레드 들과 균등하게 나눠 일을한다.
쓰레드 간의 동기화를 처리해야 해서 오버헤드가 생기지만 stop-the-world 시간이 크게 감소한다.

출처: https://v8.dev/blog/trash-talk

Incremental

메인 쓰레드가 작은양의 작업을 간헐적으로 처리한다.
가비지 컬렉션을 수행하는 시간이 분산돼, 좋은 UX의 제공이 가능하다.

출처: https://v8.dev/blog/trash-talk

Concurrent

메인 쓰레드는 가비지 컬렉션을 하지 않고 헬퍼 쓰레드가 이를 수행한다.
기술적으로 구현이 어렵지만, 메인 쓰레드는 더이상 stop-the-world가 없다는 큰 장점이 있다.

idle-time-GC

개발자는 GC에 직접 접근할 수 없다.
하지만 v8은 크롬등의 프로그램에게 가비지 컬렉션을 유발할 수 있는 메커니즘을 제공한다.

크롬은 프로그램이 쉬는 free, idle time을 알 수 있다.
예를들면, 크롬은 1초에 60프레임을 제공하는데 1프레임 렌더링에 걸리는 시간은 약 16ms(1s/60) 이다.
만약 애니메이션 프레임 렌더링 작업이 16ms보다 빨리 끝나게 된다면,
크롬은 다음 프레임 작업 전까지 가비지 컬렉션을 유발하게 된다.

출처: https://v8.dev/blog/trash-talk

정리를 마치며

오늘 정리한 내용은 목요일에 공부 했던 내용 + 조금 더 찾아본 내용을 다시 정리한 내용이다.
지금까지는 그냥 당일에 공부한건 당일에 정리하고, 다음에는 한번씩 다시 보기만 했었는데
이렇게 시간이 여유로울 때 정리를 하면 몰랐던 내용 혹은 헷갈렸던 내용도 추가로 학습해서 좋은것같다.
그래서 앞으로는 당일에 학습한 내용에 대한 정리를 마치더라도
일요일이 끝나기 전에 해당 내용에서 헷갈리는 개념등을 다시 학습해보면 좋을것같다.

참조

https://ui.toast.com/weekly-pick/ko_20200228
https://jaehyeon48.github.io/javascript/memory-management-in-v8/
https://speakerdeck.com/deepu105/v8-minor-gc?slide=1
https://speakerdeck.com/deepu105/v8-memory-usage-stack-and-heap?slide=1
https://ui.dev/ultimate-guide-to-execution-contexts-hoisting-scopes-and-closures-in-javascript
https://fehead.tistory.com/
https://v8.dev/blog/trash-talk
https://fe-developers.kakaoent.com/2022/220519-garbage-collection/

profile
기록하는 블로그

0개의 댓글

관련 채용 정보