Ahead of Time Compilation

이 문서는 Julia의 사전 컴파일(AOT) 시스템의 설계 및 구조를 설명합니다. 이 시스템은 시스템 이미지 및 패키지 이미지를 생성할 때 사용됩니다. 여기서 설명하는 구현의 대부분은 aotcompile.cpp, staticdata.cprocessor.cpp에 위치해 있습니다.

Introduction

줄리아는 일반적으로 즉시 컴파일(JIT)하지만, 코드를 미리 컴파일하고 결과 코드를 파일에 저장하는 것이 가능합니다. 이는 여러 가지 이유로 유용할 수 있습니다:

  1. Julia 프로세스를 시작하는 데 걸리는 시간을 줄이기 위해.
  2. 코드 실행 대신 JIT 컴파일러에서 소요되는 시간을 줄이기 위해 (첫 번째 실행까지의 시간, TTFX).
  3. JIT 컴파일러가 사용하는 메모리 양을 줄이기 위해.

High-Level Overview

다음 설명은 사용자가 using Foo를 입력할 때 발생하는 새로운 AOT 모듈을 컴파일할 때 내부적으로 발생하는 엔드 투 엔드 파이프라인의 현재 구현 세부 사항을 스냅샷으로 보여줍니다. 이러한 세부 사항은 더 나은 처리 방법을 구현함에 따라 시간이 지남에 따라 변경될 가능성이 있으므로 현재 구현은 아래에 설명된 데이터 흐름 및 기능과 정확히 일치하지 않을 수 있습니다.

Compiling Code Images

먼저, 네이티브 코드로 컴파일해야 하는 메서드를 식별해야 합니다. 이는 컴파일할 코드를 실제로 실행해야만 가능하며, 컴파일해야 하는 메서드의 집합은 메서드에 전달되는 인수의 유형에 따라 달라지기 때문에 특정 유형 조합의 메서드 호출은 런타임까지 알 수 없을 수 있습니다. 이 과정에서 컴파일러가 보는 정확한 메서드가 나중에 컴파일을 위해 추적되어 컴파일 추적이 생성됩니다.

Note

현재 이미지를 컴파일할 때, Julia는 AOT 컴파일을 수행하는 프로세스와 다른 프로세스에서 추적 생성을 실행합니다. 이는 사전 컴파일 중에 디버거를 사용하려고 할 때 영향을 미칠 수 있습니다. 디버거를 사용하여 사전 컴파일을 디버깅하는 가장 좋은 방법은 rr 디버거를 사용하여 전체 프로세스 트리를 기록하고, rr ps를 사용하여 관련된 실패 프로세스를 식별한 다음, rr replay -p PID를 사용하여 실패한 프로세스만 재생하는 것입니다.

메서드를 컴파일할 방법이 식별되면, 이들은 jl_create_system_image 함수에 전달됩니다. 이 함수는 네이티브 코드를 파일로 직렬화할 때 사용될 여러 데이터 구조를 설정한 다음, 메서드 배열과 함께 jl_create_native를 호출합니다. jl_create_native는 메서드에 대해 코드 생성을 실행하고 하나 이상의 LLVM 모듈을 생성합니다. 그런 다음 jl_create_system_image는 모듈에서 생성된 코드에 대한 유용한 정보를 기록합니다.

모듈은 jl_create_system_image에 의해 기록된 정보와 함께 jl_dump_native에 전달됩니다. jl_dump_native는 명령줄 옵션에 따라 모듈을 비트코드, 객체 또는 어셈블리 파일로 직렬화하는 데 필요한 코드를 포함하고 있습니다. 직렬화된 코드와 정보는 아카이브로서 파일에 기록됩니다.

마지막 단계는 jl_dump_native에 의해 생성된 아카이브의 오브젝트 파일에 시스템 링커를 실행하는 것입니다. 이 단계가 완료되면 컴파일된 코드를 포함하는 공유 라이브러리가 생성됩니다.

Loading Code Images

코드 이미지를 로드할 때, 링커에 의해 생성된 공유 라이브러리가 메모리에 로드됩니다. 그런 다음 시스템 이미지 데이터가 공유 라이브러리에서 로드됩니다. 이 데이터는 공유 라이브러리에 컴파일된 타입, 메서드 및 코드 인스턴스에 대한 정보를 포함하고 있습니다. 이 데이터는 코드 이미지가 컴파일되었을 때의 런타임 상태를 복원하는 데 사용됩니다.

코드 이미지가 다중 버전으로 컴파일된 경우, 로더는 현재 머신에서 사용 가능한 CPU 기능에 따라 각 함수의 적절한 버전을 선택합니다.

시스템 이미지의 경우, 다른 코드가 로드되지 않았기 때문에 런타임의 상태는 코드 이미지가 컴파일되었을 때와 동일합니다. 패키지 이미지의 경우, 코드가 컴파일되었을 때와 비교하여 환경이 변경되었을 수 있으므로 각 메서드는 여전히 유효한 코드인지 확인하기 위해 전역 메서드 테이블과 대조해야 합니다.

Compiling Methods

Tracing Compiled Methods

줄리아는 JIT 컴파일러에 의해 컴파일된 모든 메서드를 기록하는 명령줄 플래그 --trace-compile=filename을 제공합니다. 함수가 컴파일되고 이 플래그에 파일 이름이 지정되면, 줄리아는 호출된 메서드와 인수 유형을 포함한 사전 컴파일 문을 해당 파일에 출력합니다. 따라서 이는 AOT 컴파일 과정에서 나중에 사용할 수 있는 사전 컴파일 스크립트를 생성합니다. PrecompileTools 패키지는 패키지 개발자가 이 기능을 쉽게 활용할 수 있도록 도와주는 도구를 제공합니다.

jl_create_system_image

jl_create_system_image는 런타임의 상태를 나중에 복원하는 데 필요한 모든 Julia-specific 메타데이터를 저장합니다. 여기에는 코드 인스턴스, 메서드 인스턴스, 메서드 테이블 및 타입 정보와 같은 데이터가 포함됩니다. 이 함수는 또한 네이티브 코드를 파일에 직렬화하는 데 필요한 데이터 구조를 설정합니다. 마지막으로, jl_create_native를 호출하여 전달된 메서드에 대한 네이티브 코드를 포함하는 하나 이상의 LLVM 모듈을 생성합니다. jl_create_native는 전달된 메서드에 대해 코드 생성을 실행하는 책임이 있습니다.

jl_dump_native

jl_dump_native는 네이티브 코드를 포함하는 LLVM 모듈을 파일에 직렬화하는 역할을 합니다. 모듈 외에도 jl_create_system_image에 의해 생성된 시스템 이미지 데이터가 전역 변수로 컴파일됩니다. 이 메서드의 출력은 코드와 시스템 이미지 데이터를 포함하는 비트코드, 오브젝트 및/또는 어셈블리 아카이브입니다.

jl_dump_native는 네이티브 코드를 생성할 때 일반적으로 가장 많은 시간을 소모하는 부분 중 하나로, LLVM IR 최적화 및 기계 코드 생성을 위해 많은 시간이 소요됩니다. 따라서 이 함수는 최적화 및 기계 코드 생성 단계를 멀티스레딩할 수 있습니다. 이 멀티스레딩은 모듈의 크기에 따라 매개변수화되지만, JULIA_IMAGE_THREADS 환경 변수를 설정하여 명시적으로 재정의할 수 있습니다. 기본 최대 스레드 수는 사용 가능한 스레드 수의 절반이지만, 이를 낮게 설정하면 컴파일 중 피크 메모리 사용량을 줄일 수 있습니다.

jl_dump_native는 Julia 로더와 통합될 때 여러 아키텍처에 최적화된 네이티브 코드를 생성할 수 있습니다. 이는 JULIA_CPU_TARGET 환경 변수를 설정하고 최적화 파이프라인의 다중 버전 처리 패스를 통해 조정됩니다. 멀티스레딩과 함께 작동하도록 하기 위해, 모듈이 자체 스레드에서 방출되는 하위 모듈로 분할되기 전에 주석 단계가 추가되며, 이 주석 단계는 전체 모듈에서 사용할 수 있는 정보를 사용하여 서로 다른 아키텍처에 대해 어떤 함수가 복제될지를 결정합니다. 주석이 완료되면, 개별 스레드는 복제된 함수에 의해 호출될 필수 함수를 생성할 하위 모듈이 보장된다는 것을 알고 서로 다른 아키텍처에 대한 코드를 병렬로 방출할 수 있습니다.

모듈이 직렬화된 방법에 대한 다른 메타데이터도 아카이브에 저장됩니다. 예를 들어, 모듈을 직렬화하는 데 사용된 스레드 수와 컴파일된 함수 수가 포함됩니다.

Static Linking

AOT 컴파일 과정의 마지막 단계는 jl_dump_native에 의해 생성된 아카이브의 오브젝트 파일에 링커를 실행하는 것입니다. 이 과정은 컴파일된 코드를 포함하는 공유 라이브러리를 생성합니다. 이 공유 라이브러리는 이후 Julia에 의해 로드되어 런타임의 상태를 복원할 수 있습니다. 시스템 이미지를 컴파일할 때는 C 컴파일러에 의해 사용되는 네이티브 링커가 최종 공유 라이브러리를 생성하는 데 사용됩니다. 패키지 이미지의 경우, 더 일관된 링킹 인터페이스를 제공하기 위해 LLVM 링커 LLD가 사용됩니다.

Loading Code Images

Loading the Shared Library

코드 이미지를 로드하는 첫 번째 단계는 링커에 의해 생성된 공유 라이브러리를 로드하는 것입니다. 이는 공유 라이브러리의 경로에서 jl_dlopen을 호출하여 수행됩니다. 이 함수는 공유 라이브러리를 로드하고 라이브러리의 모든 심볼을 해결하는 역할을 합니다.

Loading Native Code

로더는 먼저 컴파일된 네이티브 코드가 로더가 실행되고 있는 아키텍처에 유효한지 확인해야 합니다. 이는 구형 CPU가 인식하지 못하는 명령어를 실행하는 것을 피하기 위해 필요합니다. 이는 현재 머신에서 사용 가능한 CPU 기능을 코드가 컴파일된 CPU 기능과 비교하여 수행됩니다. 멀티버전이 활성화되면, 로더는 현재 머신에서 사용 가능한 CPU 기능에 따라 각 함수의 적절한 버전을 선택합니다. 멀티버전화된 기능 세트가 없으면, 로더는 오류를 발생시킵니다.

다중 버전 패스의 일부는 모듈의 모든 함수에 대한 여러 개의 전역 배열을 생성합니다. 이 과정이 다중 스레드로 진행될 때, 배열의 배열이 생성되며, 로더는 이를 이 아키텍처를 위해 컴파일된 모든 함수가 포함된 하나의 큰 배열로 재구성합니다. 모듈의 전역 변수에 대해서도 유사한 과정이 발생합니다.

Setting Up Julia State

로더는 네이티브 코드를 로드하여 생성된 전역 변수와 함수를 사용하여 현재 프로세스에서 줄리아 런타임 핵심 데이터 구조를 설정합니다. 이 설정에는 줄리아 런타임에 유형과 메서드를 추가하고, 캐시된 네이티브 코드를 다른 줄리아 함수와 인터프리터에서 사용할 수 있도록 만드는 것이 포함됩니다. 패키지 이미지의 경우, 각 메서드는 검증되어야 하며, 전역 메서드 테이블의 상태가 패키지 이미지가 컴파일된 상태와 일치해야 합니다. 특히, 패키지 이미지의 로드 시간에 존재하는 메서드 집합이 컴파일 시간과 다를 경우, 메서드는 무효화되고 첫 사용 시 재컴파일되어야 합니다. 이는 패키지가 미리 컴파일되었는지 아니면 코드가 직접 실행되었는지에 관계없이 실행 의미가 동일하게 유지되도록 보장하기 위해 필요합니다. 시스템 이미지는 로드 시간에 전역 메서드 테이블이 비어 있으므로 이 검증을 수행할 필요가 없습니다. 따라서 시스템 이미지는 패키지 이미지보다 로드 시간이 더 빠릅니다.