2020년 2월 10일 월요일

헤르메스(Hermes) : 페이스북 오픈 소스 프로젝트


모바일 앱에 최적화 된 자바스크립트 엔진 오픈소스


페이스북에는 페이스북 앱의 사용자 경험을 증진시키기 위해 지속적으로 자바스크립트 코드와 플랫폼을 향상 시키는 팀이 있다. 그 팀에서 성능 데이터를 분석 한 결과 JavaScript 엔진 자체가 시작 성능 및 다운로드 크기에서 중요한 요소 인 것으로 나타났다.

이 데이터를 바탕으로 데스크톱 또는 랩톱과 비교할 때 휴대전화의 제한된 환경에서 JavaScript 성능을 최적화 해야한다는 것을 알았다. 다른 옵션을 살펴본 후 Hermes라고하는 새로운 JavaScript 엔진을 구축했다. 메모리가 제한적이고 저장 공간이 느리고 컴퓨팅 성능이 저하 된 마켓에 이미 출시된 수많은 디바이스에 이르기까지 React Native 앱에 중점을 두어 앱 성능을 향상 시키도록 설계되었다.

Hermes가 React Native 성능을 개선하는 방법

JavaScript 기반 모바일 애플리케이션의 경우 사용자 경험은 다음과 같은 몇 가지 주요 지표에 따라 달라진다.

1. 상호 작용 시간 (TTI: Time to interact)이라고하는 앱을 사용하는 데 걸리는 시간
2. 다운로드 크기 (Android, APK 크기)
3. 메모리 활용

위 주요 지표는 엔진의 Javascript 코드를 실행할 때 엔진의 CPU사용량에 대해서는 상대적으로 영향을 받지 않는다. 오늘날 대부분의 기존 Javasciprt 엔진은 각각의 서로 다른 전략 및 장단점이 있었기 때문에, 페이스북 팀은 바닥 부터 헤르메스를 설계하고 제작했다. 그 성과로 React Native 기반의 응용 프로그램을 크게 개선하였다.

Hermes는 모바일 앱에 최적화 되어 있으므로 브라우저 나 Node.js와 같은 서버 인프라와 통합 할 계획이 없다. 이러한 환경에서는 기존 JavaScript 엔진이 낫다.

헤르메스의 주요 아키텍쳐 결정 사항
적은 양의 RAM 및 느린 플래시와 같은 모바일 기기의 장치 제한에 입각해 특정 아키텍처 결정을 내렸다.

바이트 코드 사전 컴파일(Bytecode precompilation)

일반적으로 JavaScript 엔진은 로드 된 후 JavaScript 소스를 구문 분석하여 바이트 코드를 생성한다. 이 단계는 JavaScript 실행 시작을 지연 시킨다. 이 단계를 건너뛰기 위해 Hermes는 AOT 컴파일러를 사용한다.이 컴파일러는 모바일 애플리케이션 빌드 프로세스의 일부로 실행된다. 결과적으로 바이트 코드를 최적화하는 데 더 많은 시간을 쓸 수 있으므로 바이트 코드가 더 작고 효율적이다. 함수 중복 제거 및 문자열 테이블 패킹과 같은 전체 프로그램 최적화를 수행 할 수도 있다.

바이트 코드는 런타임에 전체 파일을 열심히 읽을 필요없이, 메모리에 매핑되고 해석 될 수 있도록 설계되었다. 플래시 메모리 I/O는 많은 중형 및 저가형 모바일 장치에서 상당한 대기 시간을 추가 하므로 필요할 때만 플래시에서 바이트 코드를 로드하고 크기에 맞게 바이트 코드를 최적화하면 TTI가 크게 향상된다. 또한 메모리는 파일에 의해 읽기 전용으로 매핑되고 백업되므로 Android와 같이 교체되지 않는 모바일 운영 체제는 여전히 메모리 부족으로 이러한 페이지를 제거 할 수 있다. 이렇게하면 메모리 제한 장치의 메모리 부족으로 인한 프로세스 종료, 소위 말하는 OOM으로 인한 종료가 줄어들게 된다.



압축 된 바이트 코드는 압축 된 JavaScript 소스 코드보다 약간 크지만 Hermes의 기본 코드 크기가 작기 때문에 Hermes는 Android React Native 앱의 전체 애플리케이션 크기를 줄인다.

No JIT

실행 속도를 높이기 위해, 여러 JavaScript 엔진은 자주 해석되는 코드를 기계 코드로 Lazy하게 컴파일 할 수 있다. 이 작업은 JIT (Just-In-Time) 컴파일러에서 수행한다.

현재 버전에는 JIT 컴파일러가 없다. 이는 CPU 성능에 의존적인 몇몇의 벤치마크에서는 성능이 낮음을 의미한다. 이것은 의도적인 선택이었다. 이러한 벤치마크는 일반적으로 모바일 앱의 워크 로드를 나타내는 것은 아니다. JIT는 응용 프로그램이 시작될 때 예열해야하므로 TTI를 개선하는 데 어려움이 있으며 TTI를 손상시킬 수도 있다. 또한 JIT는 기본 코드 크기 및 메모리 소비를 가중 시키게 되어 기본 메트릭에 부정적인 영향을 미친다. JIT는 팀이 가장 중요하게 생각하는 메트릭을 손상시킬 가능성이 있으므로 구현하지 않기로 결정했다. 대신에 인터프리터의 성능 향상에 집중했다.


가비지 콜렉터 전략(Garbage collector strategy)


모바일 기기에서는 특히 효율적으로 메모리를 사용하는 것이 중요하다. 저사양의 디바이스는 제한된 메모리를 가지고 있고, OS swapping이 일반적으로 존재하지 않는다. 그리고 많은 메모리를 사용하는 어플리케이션들을 공격적으로 종료시킨다. 앱이 강제 종료 되고 나면 느린 재시작이 필요하고 백그라운드 기능들이 타격을 받게 된다. 초기 단계의 테스트에서 팀은 VA(가상 주소) 공간, 특히 인접한 VA 공간은 물리적인 페이지를 느리게 할당하더라도 32비트 장치의 큰 어플리케이션에서 여전히 제한될 수 있다는 것을 알았다.

엔진의 가상주소공간 사용을 최소화 하기 위해서 아래의 기능들에 주안점을 둔 가비지 콜렉터를 만들었다.


  • 온-디멘드 할당(On-demand allocation) : 가상 주소 공간을 필요할 때만 청크 단위로 할당한다.
  • 인접하지 않도록 (Noncontinuous) : 가상 주소 공간은 단일 메모리 범위일 필요는 없으므로 32비트 장치의 리소스 제한이 없어진다. 
  • 이동(Moving) : 개체를 이동할 수 있다는 것은 메모리 조각 모음이 가능하고, 더 이상 필요 없는 청크(Chunk)가 운영체제로 반환됨을 의미한다.
  • 세대 별 (Generational) : GC마다 javascript 힙을 스캔하지 않으면 GC 시간이 줄어든다.



참고: https://engineering.fb.com/android/hermes/