본문으로 건너뛰기

백그라운드 컴파일

· 약 4분
[로스 맥일로이](https://twitter.com/rossmcilroy), 메인 스레드 옹호자

요약: Chrome 66부터 V8은 JavaScript 소스 코드를 백그라운드 스레드에서 컴파일하여 일반적인 웹사이트에서 메인 스레드가 컴파일에 소비하는 시간을 5%에서 20% 정도 감소시킵니다.

배경

Chrome은 41버전부터 V8의 StreamedSource API를 통해 JavaScript 소스 파일을 백그라운드 스레드에서 구문 분석하는 기능을 지원해 왔습니다. 이를 통해 V8은 Chrome이 네트워크로부터 파일의 첫 번째 청크를 다운로드하자마자 JavaScript 소스 코드 구문 분석을 시작하고, Chrome이 파일을 네트워크로 스트리밍하는 동안 병렬로 구문 분석을 계속 진행할 수 있습니다. 이렇게 하면 파일 다운로드가 완료될 때쯤 V8은 JavaScript 구문 분석을 거의 마칠 수 있기 때문에 로드 시간 향상에 상당한 도움이 됩니다.

하지만 V8의 초기 베이스라인 컴파일러의 한계로 인해 V8은 여전히 메인 스레드로 돌아가 구문 분석을 마무리하고 JIT 머신 코드로 스크립트를 컴파일하여 그 스크립트의 코드를 실행해야 했습니다. 새로운 Ignition + TurboFan 파이프라인으로 전환하면서 이제 바이트코드 컴파일을 백그라운드 스레드로 이동할 수 있게 되었고, Chrome의 메인 스레드를 해방시켜 더 부드럽고 반응성이 뛰어난 웹 브라우징 경험을 제공할 수 있게 되었습니다.

백그라운드 스레드 바이트코드 컴파일러 구축하기

V8의 Ignition 바이트코드 컴파일러는 파서가 생성한 추상 구문 트리(AST)를 입력으로 받아 바이트코드(BytecodeArray)의 스트림과 연관된 메타데이터를 생성하여 Ignition 인터프리터가 JavaScript 소스를 실행할 수 있도록 합니다.

Ignition의 바이트코드 컴파일러는 멀티 스레딩을 염두에 두고 설계되었지만 백그라운드 컴파일을 가능하게 하기 위해 컴파일 파이프라인 곳곳에서 여러 변경 사항이 필요했습니다. 주요 변경 사항 중 하나는 V8의 JavaScript 힙에 있는 객체를 백그라운드 스레드에서 실행하는 동안 액세스하지 못하도록 하는 것이었습니다. V8 힙에 있는 객체는 JavaScript가 단일 스레드 언어이기에 스레드 안전하지 않으며, 백그라운드 컴파일 중 메인 스레드나 V8의 가비지 컬렉터에 의해 수정될 가능성이 있습니다.

컴파일 파이프라인에서 V8의 힙에 있는 객체를 액세스하던 주요 두 단계는 AST 내부화와 바이트코드 최종화 단계였습니다. AST 내부화는 AST에서 식별된 리터럴 객체(문자열, 숫자, 객체 리터럴 보일러플레이트 등)가 V8 힙에 할당되어 스크립트 실행 시 생성된 바이트코드에 의해 바로 사용할 수 있도록 하는 과정입니다. 이 과정은 전통적으로 파서가 AST를 생성한 직후에 실행되었으며, 이후 컴파일 파이프라인 단계에서 리터럴 객체가 할당되었다는 사실에 의존하는 단계들이 있었습니다. 백그라운드 컴파일을 활성화하기 위해 우리는 AST 내부화를 컴파일 파이프라인 후반으로 이동시켜 바이트코드가 컴파일된 후에 실행하도록 변경했습니다. 이는 파이프라인 후반 단계에서 힙 내부화된 값 대신 AST에 내장된 원시 리터럴 값을 액세스하도록 수정하는 작업이 필요했습니다.

바이트코드 최종화는 함수 실행에 사용되는 최종 BytecodeArray 객체와 이와 관련된 메타데이터 — 예를 들어 바이트코드가 참조하는 상수를 저장하는 ConstantPoolArray, JavaScript 소스의 행 및 열 번호를 바이트코드 오프로 매핑하는 SourcePositionTable — 를 생성하는 작업을 포함합니다. JavaScript가 동적 언어이기 때문에 이러한 객체는 모두 JavaScript 힙에 존재해야 바이트코드와 연결된 JavaScript 함수가 수집될 경우 가비지 컬렉팅될 수 있습니다. 이전에는 이러한 메타데이터 객체 일부가 바이트코드 컴파일 동안 할당되고 수정되었으며, 이 과정에서 JavaScript 힙을 액세스해야 했습니다. 백그라운드 컴파일을 가능하게 하기 위해 Ignition의 바이트코드 생성기가 이러한 메타데이터 정보를 추적하고 컴파일의 최종 단계에서 JavaScript 힙에 할당하도록 변경되었습니다.

이 변경 사항으로 스크립트 컴파일의 거의 모든 과정이 백그라운드 스레드로 옮겨져, AST 내부화와 바이트코드 최종화의 짧은 단계만 스크립트 실행 직전에 메인 스레드에서 실행되도록 할 수 있게 되었습니다.

현재는 최상위 스크립트 코드와 즉시 호출 함수 표현식(IIFE)만 배경 스레드에서 컴파일되며, 내부 함수는 여전히 게으르게 컴파일되어(첫 실행 시) 메인 스레드에서 처리됩니다. 우리는 배경 컴파일을 더 많은 상황에 확장시키는 것을 목표로 하고 있습니다. 그러나 이러한 제한 조건 하에서도 배경 컴파일은 메인 스레드를 더 오랜 시간 동안 자유롭게 유지하여 사용자 상호작용 반응, 애니메이션 렌더링 또는 더 매끄럽고 반응적인 경험을 제공하는 기타 작업을 수행할 수 있게 합니다.

결과

우리는 실제 성능 벤치마킹 프레임워크를 사용하여 인기 있는 웹 페이지 세트를 대상으로 배경 컴파일 성능을 평가했습니다.

배경 스레드에서 이루어질 수 있는 컴파일 비율은 스트리밍 스크립트 컴파일 중 최상위 바이트코드 컴파일 비율과 내부 함수가 호출될 때 게으르게 컴파일 되는 비율(이는 여전히 메인 스레드에서 발생해야 함)에 따라 달라집니다. 따라서 메인 스레드에서 절약되는 시간 비율은 페이지마다 다르며 대부분의 웹 페이지에서는 메인 스레드 컴파일 시간의 5%에서 20% 사이의 감소를 보입니다.

다음 단계

배경 스레드에서 스크립트를 컴파일하는 것보다 더 나은 것은 무엇일까요? 바로 스크립트를 아예 컴파일할 필요가 없는 것입니다! 배경 컴파일과 함께 우리는 V8의 코드 캐싱 시스템을 개선하여 V8이 캐싱할 수 있는 코드의 양을 늘림으로써 자주 방문하는 사이트의 페이지 로딩 속도를 높이는 데에도 노력하고 있습니다. 곧 이와 관련된 업데이트를 가져다 드릴 수 있기를 바랍니다. 기대해주세요!