내가 이전에 만든 웹은 왜 그렇게 느렸을까? (JIT Compiler)
내가 이전에 만든 웹은 왜 그렇게 느렸을까? (JIT Compiler)
서론
내가 만든 웹의 성능이 안좋으면 어떡할지 걱정이 든다면 혹은 내가 만든 웹의 성능이 왜 좋은지 궁금하고 이유를 모르겠다면 이 글이 도움이 될 수 있다고 생각합니다.
브라우저의 언어
우리는 자바스크립트를 통해 브라우저와 대화합니다. 브라우저와의 대화를 하기위해서는 어떤 과정이 필요할까요?
우리는 인간이므로 우리의 언어를 사용합니다. 웹 개발은 주로 JS를 이용합니다. 안타깝게도 브라우저는 JS를 알아듣지 못하니 우린 브라우저와 소통을 하게 해주는 통역사가 필요합니다.
전통적인 통역 방식
유명한 통역방식이 있습니다. 바로 인터프리터와 컴파일러입니다. JIT 컴파일러의 경우 이 두가지 특징을 모두 가지므로 간단하게 알아보도록 하겠습니다.
인터프리터(Interpreter → 통역사)
인터프리터는 소스 코드를 한 줄씩 저수준 즉 바이트코드로 번역하여 각 줄을 즉시 처리합니다.
장점
- 실행 준비가 빠릅니다. 첫 번째 줄부터 바로 번역하고 코드를 실행합니다.
- 바로 실행하기 때문에 오류를 즉시 감지하여 디버깅이 더 쉬워지는 이점을 제공합니다.
단점
- 코드를 한 줄씩 처리하므로 번역하는 데 시간이 더 오래 걸립니다.
- 중복 번역: 동일한 코드를 여러 번 실행해야 할 때, 매번 동일한 번역 과정을 반복해야 한다는 점이 단점입니다.
컴파일러(Compiler → 번역가)
인터프리터와 달리 컴파일러는 전체 프로그램을 저수준 언어로 한 번에 번역합니다. 한 번의 작업으로 전체 프로그램을 컴파일합니다.
장점
- 초기에 코드를 한 번만 번역하기 때문에, 루프(loop)처럼 동일한 코드가 반복 실행되는 경우 더 빠르게 실행됩니다.
- 코드 번역 중 더 많은 시간을 들여 코드를 분석하고 수정하여 실행 속도를 높일 수 있습니다.(최적화)
단점
- 실행 전에 전체 컴파일 과정을 거쳐야 하기 때문에, 초기 실행 준비에 더 많은 시간이 걸립니다.
- 실시간 디버깅이 불가능합니다.
컴파일 & 인터프리터의 결과물
- 결국 컴파일러와 인터프리터는 인간의 언어를 byte-code로 파싱하는 역할을 합니다.(컴파일러의 경우는 기계어로 직접 파싱하는 경우도 존재합니다.)
통역 과정
- 어휘 분석(Lexical Analysis)
소스 코드를 문자 단위로 읽고, 이를 최소 단위인 토큰으로 분할하는 단계입니다.
- Token
- 구문 분석(Syntax Analysis)
토큰 리스트를 기반으로 코드 구조를 이해하고 AST(abstract syntax tree, 추상 구문 트리)로 변환하는 단계입니다.
- AST
- 바이트 코드 생성
AST를 기반으로 바이트 코드를 생성합니다.
- byte code
Just-In-Time Compiler (JIT)
JIT은 기본적으로 인터프리터와 컴파일러의 역할을 동시에 수행합니다.
인터프리터의 비효율성, 즉 루프를 반복 실행할 때마다 코드를 재번역해야 하는 문제를 해결하기 위해 브라우저들은 컴파일러를 결합하는 방식을 도입했습니다.
JIT은 3단계의 과정을 거칩니다.
- 프로파일러
- 기본 컴파일러
- 최적화 컴파일러
프로파일러
- 인터프리터를 통해 코드 실행
- 프로파일러가 코드 실행 패턴 관찰
- Warm 코드: 동일한 코드가 몇 번 실행되면, 그 코드 부분을 “warm” 상태라고 부릅니다.
- Hot 코드: 동일한 코드가 매우 자주 실행되면, 그 코드 부분을 “hot” 상태라고 부릅니다.
이는 코드의 실행 빈도에 따라 최적화를 적용하는 기준으로 사용됩니다.
기본 컴파일러(Baseline Compiler)
- Warm Code를 컴파일합니다.
- 컴파일 결과 저장: 컴파일된 코드는 저장되어 이후 반복 실행 시 빠르게 실행될 수 있습니다.(기계어)
이 과정은 코드 실행 성능을 개선하기 위해 자주 실행되는 함수를 효율적으로 최적화하는 방식입니다.
최적화
- 라인별 스텁 생성: “스텁(stub)“이라는 단위로 컴파일됩니다.
- 스텁들은 줄 번호와 변수 타입을 기준으로 인덱싱됩니다.
- 동일한 코드가 동일한 변수 타입으로 다시 실행되면, 프로파일러는 컴파일된 스텁을 가져와 사용합니다.
- 결과 : 실행 속도를 크게 높입니다.
이러한 방식은 실행 성능을 극대화하면서도 초기 실행 시간을 최소화하는 데 도움을 줍니다.
최적화 컴파일러(Optimizing Compiler)
- Hot Code를 컴파일합니다.
- 최적화 컴파일러는 해당 코드를 더욱 빠르고 효율적인 버전을 생성합니다.
- 최적화 컴파일러에서는 함수 전체를 한꺼번에 컴파일합니다
더 빠른 버전의 코드를 만들기 위해 최적화 컴파일러는 몇 가지 가정을 해야 합니다.
- 동일한 Shape 사용
- 객체가 가진 속성과 속성의 추가 순서에 따라 객체의 구조적 형태(Shape)가 정의됩니다.
- JavaScript 엔진은 같은 구조를 가진 객체들을 효율적으로 처리하기 위해 동일한 Shape을 공유하도록 최적화합니다.
- 같은 Shape을 가진 객체는 메모리 공간을 공유합니다.
Shape 예제
- 모니터링과 추론:
JIT 컴파일러는 코드를 모니터링합니다. 반복문 등을 통해 수집된 정보를 바탕으로 패턴을 추론합니다.
과거 실행에서 특정 조건이 항상 참이었다면, 이후에도 그럴 것이라 가정하고 최적화된 코드를 생성합니다.
- 검증과 코드 폐기
JavaScript의 동적 특성상 99개의 객체가 모두 같은 형태를 가졌더라도, 100번째 객체에서 속성이 누락되거나 형태가 달라질 수 있습니다. 이 경우, 최적화된 코드는 실행 전에 가정을 검증합니다.
가정이 유효하면 최적화된 코드가 실행되지만, 그렇지 않으면 JIT 컴파일러는 해당 코드를 “잘못된 최적화”로 간주하고 폐기합니다. 이 경우 이전 버전으로 돌아갑니다.(이를 deoptimization 또는 bailout이라고 함)
즉, JIT 컴파일러는 동일한 패턴을 최대한 활용하여 성능을 높이려고 하지만, 언제든 잘못된 가정이 발생할 수 있다는 점에서 신중하게 동작합니다.
문제점
만약 코드가 최적화와 디옵티마이제이션을 반복한다면, 오히려 기본 컴파일된 버전을 실행하는 것보다 더 느려질 수 있습니다.
대부분의 브라우저는 이러한 최적화/디옵티마이제이션 사이클에서 벗어나는 한계를 추가했습니다. 예를 들어, JIT 컴파일러가 10번 이상 최적화를 시도했지만 매번 폐기해야 하는 상황이 반복된다면, 더 이상 최적화를 시도하지 않도록 설계되었습니다.
deoptimization 기준
- 객체 Shape의 변경
- 타입 변경
- 프로토타입의 변경
- 함수 인라인 취소
함수
인라인화
예상치 못한 호출로 인라인화가 취소될 경우
- eval 또는 with의 사용
eval과 with는 런타임에 변수나 스코프를 동적으로 변경하기 때문에, 최적화된 코드를 사용할 수 없게 만듭니다.
- 함수의 인자 개수 변화
- 배열의 비일관성
- try-catch 블록
try-catch 블록은 예외 처리로 인해 런타임 동작이 복잡해지므로 최적화된 경로에서 제외됩니다.
최적화 유지 방법
- 객체의 Shape을 고정
- 일관된 타입 사용
- 배열의 타입 일관성 유지
- eval과 with 사용 금지
- try-catch블록을 꼭 필요할때 사용할 필요
- try-catch블록 내부에 반복문과 같은 높은 메모리를 사용하는 코드를 사용하지 않도록 주의
후기
예전 프로젝트의 경우 Vue로 개발했음에도 불구하고 현재 React프로젝트보다 성능이 떨어진다고 느끼고 있었습니다. 그 이유에 대해서는 명확하지 않았습니다. Vue의 경우 컴파일러가 따로 존재하여 오히려 더 성능이 좋아야 하지 않을까? 하는 의문도 있었구요.
그러나 지금 JIT 컴파일러를 공부해보니 그 이유가 어느정도 파악이 되는 것 같습니다. 현재는 단순히 “정적인 것이 성능에 좋다”라고 알고있었고 그래서 최대한 정적인 코드를 작성하며 런타임에서 객체를 변형하거나 타입이 변경되는 것을 하지 않고 있습니다.
이전에 Vue로 개발할 때에는 런타입에서 TS를 엄격하게 사용하지 않기도 했고 객체의 속성을 런타임에 변경하는 등의 행동을 했던 기억이 있습니다. 현재 개발하는 코드가 성능이 더 좋다는 것은 알았지만 우연히 명확한 이유에 대해서 이렇게 알게되어 기쁘기도 하고 공부의 부족함도 많이 느끼게 됩니다.
출처 : ** **et's Understand the JavaScript Just In Time Compiler (JIT) and How the V8 engine works
A crash course in just-in-time (JIT) compilers
: https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/
자바스크립트와 JIT 컴파일

