Android 8.0 ART 개선사항

Android 8.0 버전에서 Android 런타임(ART)이 대폭 개선되었습니다. 아래 목록은 기기 제조업체가 ART에서 기대할 수 있는 개선 사항을 요약한 것입니다.

동시 압축 가비지 컬렉터

Google I/O에서 발표된 대로, ART는 Android 8.0에서 새로운 동시 압축 가비지 컬렉터(GC)를 제공합니다. 이 컬렉터는 GC가 실행될 때마다 힙을 압축하고, 앱이 실행 중인 동안에는 스레드 루트를 처리하기 위해 한 번만 짧게 일시중지합니다. 컬렉터의 장점은 다음과 같습니다.

  • GC는 항상 힙을 압축하기 때문에 Android 7.0에 비해 평균 힙 크기가 32% 더 작습니다.
  • 압축을 통해 스레드 로컬 범프 포인터 객체 할당이 가능하며 Android 7.0에 비해 할당이 70% 더 빠릅니다.
  • H2 벤치마크의 일시중지 시간이 Android 7.0 GC보다 85% 더 짧습니다.
  • 일시중지 시간이 더 이상 힙 크기에 비례해 길어지지 않기 때문에 앱에서 버벅거림에 관한 걱정 없이 대량의 힙을 사용할 수 있습니다.
  • GC 구현 세부정보 - 읽기 장벽
    • 읽기 장벽은 읽은 객체 필드 각각에 수행되는 소규모 작업입니다.
    • 이는 컴파일러에서 최적화되지만 일부 사용 사례의 속도가 느려질 수 있습니다.

루프 최적화

Android 8.0 버전에서 ART는 다양한 루프 최적화를 사용합니다.

  • 경계 확인 제거
    • 정적: 범위는 컴파일 시간에 경계 내에 있는 것으로 확인됩니다.
    • 동적: 런타임 테스트를 통해 루프가 경계 내에 있도록 합니다(그렇지 않으면 탈최적화).
  • 유도 변수 제거
    • 불량 유도 삭제
    • 루프 이후에만 사용된 유도를 닫힌 형식의 표현식으로 변경
  • 루프 본문 내의 불량 코드 제거, 불량 상태인 루프 전체 삭제
  • 강도 감소
  • 루프 변환: 반전, 교환, 분할, 언롤링, 유니모듈러 등
  • SIMDization(벡터화라고도 함)

루프 최적화 도구는 ART 컴파일러의 자체 최적화 단계에 있습니다. 대부분의 루프 최적화는 다른 영역의 최적화 및 단순화와 유사합니다. 대부분의 CFG 유틸리티(nodes.h 참고)는 CFG를 다시 작성하는 것이 아니라 빌드하는 데 초점을 맞추기 때문에 일반적인 복잡한 방식 이상으로 CFG를 다시 쓰는 일부 최적화와 관련해 문제가 발생합니다.

클래스 계층 구조 분석

Android 8.0의 경우 ART는 클래스 계층 구조를 분석하여 생성된 정보를 기반으로 가상 호출을 직접 호출로 탈가상화하는 컴파일러 최적화인 클래스 계층 구조 분석(CHA)을 사용합니다. 가상 호출은 vtable 조회를 중심으로 구현되고 몇몇 종속 로드를 가져오기 때문에 비용이 많이 듭니다. 또한 가상 호출은 인라인되지 않습니다.

다음은 이와 관련한 개선사항의 요약입니다.

  • 동적 단일 구현 메서드 상태 업데이트 - 클래스 연결 시간이 끝날 때 vtable이 채워지면 ART는 슈퍼 클래스의 vtable과 항목을 하나씩 비교합니다.
  • 컴파일러 최적화 - 컴파일러는 메서드의 단일 구현 정보를 활용합니다. 메서드 A.foo에 단일 구현 플래그 세트가 있는 경우 컴파일러는 가상 호출을 직접 호출로 탈가상화하고 직접 호출을 인라인하려고 합니다.
  • 컴파일된 코드 무효화 - 클래스 연결 시간이 끝날 때 단일 구현 정보가 업데이트되면 이전에 단일 구현이 있었으나 해당 상태가 무효화된 메서드 A.foo의 경우 메서드 A.foo가 단일 구현이 있다고 가정하는 컴파일된 모든 코드는 컴파일된 코드를 무효화해야 합니다.
  • 탈최적화 - 스택에 있는 컴파일된 라이브 코드의 경우 탈최적화가 시작되면 컴파일된 코드 중 무효화된 코드를 인터프리터 모드로 만들어 정확성을 보장합니다. 동기 및 비동기 탈최적화를 혼용하는 새로운 탈최적화 메커니즘이 사용됩니다.

.oat 파일의 인라인 캐시

이제 ART는 인라인 캐시를 사용하고 데이터가 충분한 호출 사이트를 최적화합니다. 인라인 캐시 기능은 추가 런타임 정보를 프로필에 기록하고 이 정보를 사용하여 동적 최적화를 AOT(Ahead of Time) 컴파일에 추가합니다.

Dexlayout

Android 8.0에 도입된 라이브러리인 Dexlayout은 dex 파일을 분석하고 프로필에 따라 재정렬하는 라이브러리입니다. Dexlayout은 런타임 프로파일링 정보를 사용하여 기기의 유휴 유지보수 컴파일 중에 dex 파일의 섹션을 재정렬하는 것을 목표로 합니다. 종종 함께 액세스되는 dex 파일의 일부를 그룹화하여 개선된 지역성을 통해 프로그램에 향상된 메모리 액세스 패턴을 제공하면 RAM을 절약하고 시작 시간을 단축할 수 있습니다.

현재 프로필 정보는 앱이 실행된 후에만 사용할 수 있으므로 dexlayout은 유휴 유지보수 중에 dex2oat의 기기 내 컴파일에 통합됩니다.

Dex 캐시 삭제

Android 7.0까지는 DexFile의 특정 요소 개수에 맞춰 DexCache 객체에 큰 배열 네 개가 있었습니다. 이러한 배열은 다음과 같습니다.

  • 문자열(DexFile::StringId당 참조 1개),
  • 유형(DexFile::TypeId당 참조 1개),
  • 메서드(DexFile::MethodId당 네이티브 포인터 1개),
  • 필드(DexFile::FieldId당 네이티브 포인터 1개).

이러한 배열은 이전에 확인된 객체를 빠르게 검색하는 데 사용되었습니다. Android 8.0에서는 메서드 배열을 제외한 모든 배열이 삭제되었습니다.

인터프리터 성능

어셈블리 언어로 작성된 핵심 가져오기/디코딩/해석 메커니즘을 사용하는 인터프리터인 'mterp'가 도입되면서 Android 7.0에서 인터프리터 성능이 크게 향상되었습니다. mterp는 빠른 Dalvik 인터프리터를 모델링한 것으로 arm, arm64, x86, x86_64, mips, mips64를 지원합니다. 컴퓨팅 코드의 경우 Art의 mterp는 Dalvik의 빠른 인터프리터와 유사하다고 볼 수 있습니다. 하지만 다음과 같은 상황에서는 속도가 훨씬 더 느려질 수 있습니다.

  1. 성능을 호출합니다.
  2. 문자열 조작 및 Dalvik의 내장 기능으로 인식되는 메서드의 헤비 유저가 존재합니다.
  3. 스택 메모리 사용량이 증가합니다.

Android 8.0은 이러한 문제를 해결합니다.

인라이닝 추가

Android 6.0 이후 ART는 동일한 dex 파일 내에서 모든 호출을 인라인할 수 있지만 다른 dex 파일의 리프 메소드만 인라인한다는 한계가 있었습니다. 이러한 한계의 원인은 두 가지가 있습니다.

  1. 다른 dex 파일에서 인라인하려면 호출자의 dex 캐시를 재사용할 수 있는 동일한 dex 파일의 인라인과 달리 다른 dex 파일의 dex 캐시를 사용해야 합니다. dex 캐시는 정적 호출, 문자열 로드 또는 클래스 로드 등 몇몇 명령어에 관한 컴파일된 코드에 필요합니다.
  2. 스택 맵은 현재 dex 파일 내의 메서드 색인만 인코딩합니다.

Android 8.0에서는 이러한 한계점을 다음과 같이 해결합니다.

  1. 컴파일된 코드에서 dex 캐시 액세스를 삭제합니다. 'Dex 캐시 삭제' 섹션도 참고하세요.
  2. 스택 맵 인코딩을 확장합니다.

동기화 개선

ART팀은 MonitorEnter/MonitorExit 코드 경로를 조정하고 기존 메모리 장벽에 관한 ARMv8의 의존도를 줄여 가능한 경우 이를 새로운 (획득/해제) 명령어로 대체했습니다.

더욱 빨라진 네이티브 메서드

@FastNative@CriticalNative 주석을 사용하여 Java 네이티브 인터페이스(JNI)에 관한 네이티브 호출이 더 빨라집니다. 기본으로 제공되는 ART 런타임 최적화를 통해 JNI 전환 속도를 높이고 현재 지원 중단된 !bang JNI 표기법을 대체합니다. 이 주석은 네이티브가 아닌 메서드에 영향을 미치지 않으며 bootclasspath의 플랫폼 Java 언어 코드에서만 사용할 수 있습니다(Play 스토어 업데이트 없음).

@FastNative 주석은 비정적 메서드를 지원합니다. 메서드가 매개변수 또는 반환 값으로 jobject에 액세스하는 경우 사용합니다.

@CriticalNative 주석을 사용하면 네이티브 메서드를 훨씬 더 빠르게 실행할 수 있으며 다음과 같은 제한이 있습니다.

  • 메서드는 정적이어야 하고 매개변수, 반환 값 또는 암시적 this의 객체일 수 없습니다.
  • 기본형만 네이티브 메서드에 전달됩니다.
  • 네이티브 메서드는 함수 정의에서 JNIEnvjclass 매개변수를 사용하지 않습니다.
  • 메서드는 동적 JNI 연결에 의존하지 않고 RegisterNatives에 등록해야 합니다.

@FastNative는 네이티브 메서드 성능을 최대 3배, @CriticalNative는 최대 5배 향상할 수 있습니다. 예를 들어 Nexus 6P 기기에서 측정된 JNI 전환은 다음과 같습니다.

Java 네이티브 인터페이스(JNI) 호출 실행 시간(나노초 단위)
일반 JNI 115
!bang JNI 60
@FastNative 35
@CriticalNative 25