Home

읽기 설정

안녕하세요, 리디아 할리입니다. 오늘 저는 저희가 만든 사람이 읽기 쉬운 자바스크립트 파일부터 컴퓨터가 이해할 수 있는 수준까지, 그 과정에서 일어나는 모든 일들을 전반적으로 살펴보는 시간을 갖겠습니다.00:00

이 과정에는 정말 많은 부분들이 있는데, 일단은 브라우저 관련 부분과 V8 엔진 관련 부분, 딱 두 가지에만 집중해서 설명할게요. V8은 크롬 기반 브라우저와 노드에서 사용되는 자바스크립트 엔진입니다.00:11

자, 우선 맨 처음으로 돌아가 봅시다. 우리가 작은 calc.js 스크립트를 사용하는 웹사이트를 로드하려고 하는 상황이에요.00:24

웹사이트 로딩을 시도하는 동안, HTML 파서는 스크립트 태그를 만나 calc.js 파일을 네트워크, 캐시, 또는 미리 가져온 서비스 워커를 통해 가져오려고 시도합니다.00:31

어쨌든 바이트 스트림이 반환되고, 바이트 스트림 디코더, 엘릭서로 전달됩니다. 이 부분은 바이트 스트림을 디코딩하고, 받은 데이터에 기반하여 토큰을 생성하는 파서의 일부입니다.00:42

예를 들어, 바이트가 F-U-N-C-T-I-O-N으로 디코딩된다는 것을 인지하면, '이 함수는 JavaScript에서 키워드라는 것을 알아.'라고 토큰을 생성하는 방식입니다.00:55

그리고 그 내용을 기반으로 토큰을 만들고, 스트림의 나머지 부분에서도 계속 그렇게 할 겁니다.01:06

토큰을 생성하는 동시에, 이 토큰들을 파서로 전부 전달합니다. 그리고 파서는 JavaScript의 특정 구문 규칙에 따라 토큰을 기반으로 노드를 생성합니다.01:12

예를 들어 변수 선언이나 함수 정의와 같은 것들을 기반으로, 파서는 프로그램의 추상 구문 트리를 생성합니다. 이 트리는 매우 단순화된 버전이며, 실제로는 프로그램에 대한 추가적인 정보도 포함합니다. 하지만 현재로서는 이 정도만 알아도 충분합니다.01:23

이 정도면 충분하고, 동시에 구문 오류도 검사합니다. 토큰 자체는 유효할 수 있지만, 특정 구문 규칙에 맞지 않을 수도 있으니까요. 마지막으로, 이 AST가 실제로 V8의 점화 엔진으로 전달되어 작동하도록 하는 역할을 합니다.01:41

인터프리터인데, 이 인터프리터는 AST를 기반으로 바이트코드를 생성하는 역할을 합니다. 그리고 노드에서 `print bytecode` 플래그를 사용하면 생성되는 바이트코드를 실제로 확인할 수 있습니다. 예를 들어, 객체와 함께 호출했을 때 `calc` 함수의 바이트코드는 이렇습니다.01:56

xy와 z 키를 포함하는 텍스트라면 대략 이런 형태를 띄게 될 것이고, 이 데이터가 많아 보일 수도 있지만, 사실 정말 중요한 부분은 두 가지뿐입니다.02:15

이그니션은 바이트코드를 실행하기 위해 레지스터를 사용합니다. R0나 R1 같은 레지스터도 있지만, 바이트코드들이 입력 및 출력을 위해 사용하는 누적 레지스터도 있습니다.02:23

그리고 함수에 전달된 값들을 위해 a0 같은 레지스터도 있습니다. 바이트코드를 살펴보면서 이게 더 명확해질 테니 걱정하지 마세요. 이 경우, x, y, 그리고 z 키를 포함하는 객체를 함수에 전달했습니다.02:35

그래서 생성된 출력의 두 번째 부분이 중요한 이유는 A0이 객체에 전달된 함수의 속성을 어디에서 찾을 수 있는지에 대한 정보를 담고 있는 모양 테이블을 가리키기 때문입니다.02:48

자, 그럼 이제 바이트코드들이 실제로 뭘 하는지 살펴봅시다. 아주 첫 번째 줄에서, 코드에 의해 특정 속성을 로드하는 LDA를 보게 됩니다.03:01

LDA에서는 값이 누적기에 로드되고, 해당 값이 함수에 전달되는 객체의 명명된 속성이라는 것을 지정합니다. 이 객체는 0번 저장소에 위치합니다.03:07

그리고 해당 속성 자체는 인덱스 0에서 찾을 수 있습니다. 그래서 인덱스 0에 있는 값이 x에 매핑된다는 것을 알 수 있죠.03:17

우리는 함수에 전달하는 객체의 x 속성 값을 불러와서 사용합니다.03:24

자, 이 경우에는 숫자 값 10입니다. 그런 다음 누적기에 있는 현재 값을 작은 정수 50을 곱합니다. 그리고 스타 r0은 누적기의 현재 값이 레지스터 r0에 저장되어야 한다는 것을 지정합니다. 다시 속성을 로드하여 누적기에 저장하는데, 이번에는요.03:29

두 번째 인덱스가 y를 가리키는데, y의 값은 20이므로 누적기의 값은 이제 20이 됩니다. 그리고 별표 2는 다시 현재 누적기의 값을 레지스터 r2에 저장하라는 것을 의미합니다. 다시 a1, 아니 a0에서 명명된 속성을 누적기에 로드합니다.03:49

이번에는 세 번째 인덱스에 매핑되며, z 값으로 30에 해당합니다.04:08

그래서 누적기의 현재 값에 레지스터 r2에 저장된 값을 곱합니다. 다음 단계에서는 레지스터 r0에 저장된 값을 누적기의 현재 값에 더해야 합니다.04:13

그러니까 500 더하기 600은 1100이 된다는 뜻이에요.04:25

마지막으로, 누적 변수의 값인 1100을 반환합니다. 바이트코드 생성기가 생성한 바이트코드 역시 몇 가지 소규모 최적화를 거치고, 이후 실제 바이트코드가 실행되면서, 저희의 기기에서 실행할 수 있게 됩니다. 결국, 저희 기기에서 처리할 수 있는 결과물이 나오는 거죠.04:30

바이트코드에서 일부 내용을 건너뛴 게 있을 거예요. 자, 왜 그런지 한번 살펴봅시다.04:47

실제로 이건 V8의 최적화 과정의 일부입니다. 왜냐하면, 우리가 X, Y, Z 객체와 같은 객체를 V8에 넘길 때, V8은 해당 객체 구조에 대한 shape을 생성하기 때문입니다.04:54

만약 문서를 읽거나 블로그 게시물을 보고 있다면, 이걸 숨겨진 클래스나 맵이라고 부르기도 해요. 그런데 이게 좀 헷갈리는 게, 자바스크립트에도 클래스와 맵이 있잖아요. 하지만 이건 자바스크립트 클래스나 맵이 아니에요, 그냥 형태일 뿐이죠.05:07

자바스크립트에서는 기본적으로 그런 구조가 없기 때문에 셰이프를 사용하는 게 훨씬 효율적입니다. 셰이프는 기본적으로 객체의 구조를 나타내는 것입니다.05:22

그리고 이 형태는 객체 내의 속성 값들을 찾을 수 있는 오프셋에 대한 포인터를 담고 있습니다.05:31

X, Y, Z 속성만 지정했음에도 불구하고, 훨씬 더 많은 내장 속성들과 객체들이 모두 메모리 어딘가에 위치 정보가 저장되어 있다는 점을 기억해야 합니다.05:37

객체 속성에 접근하려고 할 때, 예를 들어 X에 접근하려고 할 때, 이제 객체가 동일한 셰이프를 가지고 있는지 확인하는 방식으로 더 빠르게 가져올 수 있습니다. 응, 그렇군요. 좋아요. 이제 X를 가져오려고 하니, 오프셋을 알아야겠네요.05:48

모양은 V8이 사용하는 인라인 캐싱이라는 최적화 기법에 정말 유용하게 활용됩니다.06:01

인라인 캐싱은 이전 작업의 결과를 저장해두는 방식으로, 동일한 작업을 다시 호출할 때 이미 결과를 알고 있게 됩니다. 이제 프로퍼티 조회(property lookup)를 할 때마다, 이전 조회 시 사용했던 오프셋(offset)에 대한 결과를 저장해둘 수 있습니다.06:06

그래서 앞으로 동일한 작업을 수행하려고 할 때, 인라인 캐시에서 바로 결과를 가져올 수 있게 됩니다. 이 인라인 캐시는 인터프리터에게도 도움이 될 뿐만 아니라, 정말 귀중한 피드백을 제공하기도 합니다.06:22

터보팬 최적화기입니다. 그래서 드디어 바이트코드 예제로 돌아갈 수 있는데, 이 값들은 함수 실행에 대한 정보를 저장하는 피드백 벡터 슬롯에 대한 참조들입니다.06:37

여기에는 지금까지 우리가 숫자를 더해서 숫자 값을 얻는 것과 같은 산술 연산에 대한 정보도 포함됩니다.06:50

이것의 실제로 사용 가능한 예는 JavaScript에서 플러스 연산자를 사용하여 문자열을 연결할 수 있다는 점인데, 내부적으로는 훨씬 다른 방식으로 처리해야 한다는 것입니다.06:59

아직까지는 숫자 값만 있었다는 걸 알고 있어요. 괜찮아요. 이제 calc 함수를 수백 번 호출한다고 가정해 봅시다.07:09

이 기능은 지금 매우 인기 있는 상태인데, 바이트코드 자체도 이미 매우 빠르지만, V8은 더 빠른 기계어를 생성하기 위해 TurboFan, 즉 TurboFan 최적화기를 활용하기 때문입니다.07:17

바이트 코드와 특정 코드 블록에 대한 생성된 피드백을 기반으로, 해당 기기에 최적화된 기계어를 생성할 수 있습니다. 그래서 함수를 다시 호출할 때, 그냥 이 부분을 건너뛸 수 있죠.07:30

바이트코드로 변환하는 대신 즉시 기계어를 실행할 수는 있지만, 자바스크립트의 한 가지 문제는 동적 타이핑이라는 점입니다. calc 함수를 수백, 수천 번 호출할 수 있지만, 항상 이렇게 동작할 것이라는 보장은 전혀 없습니다.07:46

미래에는 또 다른 경우도 있을 수 있습니다. 예를 들어, calc 함수를 아무것도 없는 객체로, 혹은 x 키 하나만으로, 아니면 x와 y 키만으로 호출할 수도 있습니다.08:02

왜 그런 짓을 하시는지 잘 모르겠지만, 가능성은 있어요. 그래서 다양한 오브젝트 종류마다 VA는 새로운 속성을 포함하는 새로운 셰이프를 생성합니다. 이전에 인라인 캐시에는 오브젝트의 셰이프 값을 나타내는 필드와 함께, 그에 상응하는 오프셋이 저장되어 있는 것을 보셨죠.08:10

다만, 여러 객체를 통과할 경우 여러 도형이 생성되면, 인라인 캐시도 여러 도형과 그 오프셋을 가리키도록 업데이트해야 합니다.08:25

자, 이제 특정 객체에서 프로퍼티를 로드하려고 할 때, 우선 해당 프로퍼티를 포함하는 객체를 찾기 위해 가능한 모든 형태를 쭉 살펴봐야 합니다. 이는 선형 탐색으로 이어질 수 있는데, 효율적이지는 않지만요.08:35

이전에, X, Y, Z 키를 가진 객체 한 종류로만 호출될 때를 가정하고 calc 함수에 대한 기계어 코드를 생성했었는데요.08:48

하지만, 만약 calc 함수를 다시 호출하되 다른 모양의 입력으로 사용한다면, TurboFan의 모양 검사가 실패하게 됩니다. 이 경우, 최적화된 기계 코드를 더 이상 사용할 수 없게 되고, 결국 생성된 바이트코드로 다시 디옵티마이즈해야 합니다.08:57

이 수술은 꽤 비싸서, 가능한 한 피하는 게 좋을 거예요.09:11

그래서 calc 함수 내 캐시도 이제 여러 도형을 포함하고 있다는 걸 알려주도록 업데이트되었어요.09:15

이제 calc 함수는 다시 최적화될 수 있으며, 심지어 비최적화 후에도 효율적으로 작동할 수 있습니다. 물론 TurboFan은 다양한 형태를 만나면 조금 다른 방식으로 처리해야 합니다. 또한, 인라인 캐시 역시 여러 상태를 가질 수 있는데, 인라인 캐시가 한 종류의 객체만 봤을 경우 모노모픽(단형태)으로 간주됩니다.09:22

거의 최상의 시나리오라고 할 수 있는데, 왜냐하면 그 경우에 최적화된 기계 코드를 바로 생성할 수 있고, 앞으로는 이 함수가 동일한 객체 형태로 계속 호출될 거라고 예상할 수 있기 때문입니다. 만약 캐시가 두 개 또는 네 개의 서로 다른 형태를 가진다면, 이는 다형적이라고 할 수 있습니다.09:40

만약 우리가 계속해서 무작위 타입으로 계속 호출한다면, 이건 '메가모르픽'으로 간주되는데, 그럴 바엔 그냥 포기하고 최적화를 해보려고 할 것 같네요.09:57

보시다시피, 개발자로서 가끔 JavaScript의 동적 타입이 좋긴 하지만, 컴파일러에게는 그리 좋지 않죠. 결국은 추측에 의존해서 앞으로는 같은 타입의 데이터를 사용할 거라고 가정해야만 합니다.10:06

지금 제가 좀 빠르게 설명했지만, 브라우저에서 스크립트를 로드하는 것부터 시작해서 여러분의 기계에서 최적화된 기계 코드가 실행되는 것까지, 모든 과정을 살펴봤습니다. 그리고 제가 간단히 언급하고 싶은 건, 정말 많은 훌륭한 자료들이 있다는 점입니다.10:21

V8 내부 구조나 오픈 소스에 대해 더 자세히 알고 싶다면, 소스 코드를 직접 확인해 보세요. 아무튼, 시청해 주셔서 정말 감사하고 코딩 즐겁게 하세요!10:34

AI Summary

이 영상은 웹 브라우저에서 자바스크립트 파일이 실행되는 과정을 자세히 설명해요. 자바스크립트 파일 로딩부터 파싱, 바이트코드 생성 및 실행, 그리고 V8 엔진의 최적화 과정까지 꼼꼼하게 다루고 있어요. 특히, Shape, 인라인 캐싱, TurboFan과 같은 핵심 최적화 기술들과 다형성이 성능에 미치는 영향을 살펴보고, JavaScript의 동적 타이핑 특성으로 인한 최적화의 어려움과 해결 방안을 보여준답니다.

Key Highlights

  • 자바스크립트 파일은 `<script>` 태그를 통해 로딩되고, 파싱 과정을 거쳐 AST를 생성해요.
  • V8 엔진은 AST를 바이트코드로 변환하고, 레지스터를 사용하여 실행하며 최적화를 수행해요.
  • Shape는 객체 구조에 대한 정보를 담고 있으며, 인라인 캐싱은 이전에 계산된 결과를 재사용하여 성능을 향상시키죠.
  • TurboFan은 바이트코드와 피드백을 기반으로 기계어를 생성하여 JavaScript의 성능을 높이는 역할을 해요.
  • 다형성은 최적화에 어려움을 줄 수 있지만, 인라인 캐시를 통해 유연하게 처리될 수 있어요.

Related Videos