개발자

스위프트, 러스트, 클랭을 떠받치는 힘, LLVM 알아보기

Serdar Yegulalp  | InfoWorld 2020.03.13
새로운 언어와 함께 기존 언어의 개선도 개발 환경 전반적으로 급속도로 확산되고 있다. 모질라 러스트(Rust), 애플의 스위프트(Swift), 젯브레인스의 코틀린(Kotlin), 그리고 다른 많은 언어가 개발자에게 속도, 안전, 편의성, 이식성, 성능에 대한 새로운 여러 선택권을 제공한다.
 
왜 지금일까? 한 가지 중요한 이유는 언어를 구축하기 위한 새로운 툴, 특히 컴파일러에 있다. 그 중에서 선두는 스위프트 언어를 만든 크리스 래트너에 의해 일리노이 대학 연구 프로젝트로 처음 개발된 오픈소스 프로젝트, LLVM이다.
 
LLVM은 새로운 언어를 만들기 쉽게 해줄 뿐만 아니라 기존 언어의 개발도 강화한다. 언어를 만들 때 가장 힘든 부분인 컴파일러 만들기, 산출된 코드를 여러 플랫폼과 아키텍처로 이식하기, 벡터화와 같은 아키텍처별 최적화 생성하기, 그리고 예외와 같은 일반적인 언어 메타포를 처리하는 코드 쓰기의 상당부분을 자동화하는 툴을 제공한다. 또한 진보적인 라이선스 덕분에 소프트웨어 구성요소로 자유롭게 재사용하거나 서비스로 배포할 수 있다.
 
여러 유력 언어가 LLVM을 활용한다. 애플의 스위프트 언어는 LLVM을 컴파일러 프레임워크로 사용하며, 러스트는 LLVM을 툴 체인의 핵심 구성요소로 사용한다. 또한 많은 컴파일러에 LLVM 에디션이 있다. C++ 컴파일러인 클랭(그래서 “C-lang”이라는 이름이 붙음)은 그 자체가 LLVM과 긴밀히 연계된 프로젝트다. 닷넷 구현인 모노(Mono)에는 LLVM 백엔드를 사용해 네이티브 코드로 컴파일하는 옵션이 있다. 명목상 JVM 언어인 코틀린은 LLVM을 사용해 기계 네이티브 코드로 컴파일하는 코틀린 네이티브라는 이름의 버전을 개발 중이다.
 

LLVM의 정의

LLVM은 기본적으로 기계 네이티브 코드를 프로그램에 따른 방식으로 만들기 위한 라이브러리다. 개발자는 API를 사용해서 중간 표현(IR, intermediate representation)이라는 형식으로 명령어를 생성한다. LLVM은 이 IR을 독립적 바이너리로 컴파일하거나 JIT(Just-In-Time) 컴파일을 수행해서 언어를 위한 인터프리터 또는 런타임과 같은 다른 프로그램의 컨텍스트에서 실행할 수 있다.
 
LLVM의 API는 프로그래밍 언어에 사용되는 많은 일반적인 구조와 패턴을 개발하기 위한 프리미티브를 제공한다. 예를 들어 거의 모든 언어에는 함수와 전역 변수의 개념이 있고, 또한 많은 언어에 코루틴과 C 외부 함수 인터페이스가 있다. LLVM에는 함수와 전역 변수가 IR의 표준 요소로 있으며 코루틴을 생성하고 C 라이브러리와 접속하기 위한 메타포가 있다.
 
따라서 이미 있는 것을 새로 만드느라 시간과 에너지를 낭비할 필요 없이 LLVM의 구현을 사용하고, 개발자는 실질적으로 중요한 부분에 에너지를 집중할 수 있다.
 

LLVM: 이식성을 위한 설계

LLVM을 이해하려면 C 프로그래밍 언어와의 유사점을 살펴보는 것이 좋다. C는 이식 가능한 고수준 어셈블리 언어로 기술되곤 한다. 시스템 하드웨어에 긴밀하게 매핑이 가능한 구조가 있고, 그동안 거의 모든 시스템 아키텍처로 이식됐기 때문이다.
 
반면 LLVM의 IR은 처음부터 이식 가능한 어셈블리로 설계됐다. 이러한 이식성을 달성하는 방법 중 하나로 LLVM은 특정 기계 아키텍처에 구속되지 않는 프리미티브를 제공한다. 예를 들어 정수 형식은 기반 하드웨어의 최대 비트 폭(32비트 또는 64비트)으로 제한되지 않는다. 예를 들어 128비트 정수와 같이 원하는 만큼 많은 비트를 사용해서 프리미티브 정수 형식을 만들 수 있다. 또한 출력을 특정 프로세서의 명령어 집합과 맞추는 부분도 LLVM이 알아서 해주므로 신경 쓸 필요 없다. 
 
LLVM은 아키텍처 중립적 설계 덕분에 종류를 불문하고 현재와 미래의 하드웨어를 더 쉽게 지원할 수 있다. 예를 들어 IBM은 최근 LLVM의 C, C++, 포트란 프로젝트를 위한 AIX 아키텍처와 z/OS, 리눅스 온 파워(Linux on Power)를 지원하기 위한 코드를 LLVM에 기여했다(IBM의 MASS 벡터화 라이브러리 지원 포함).
 
LLVM IR의 실제 예시를 보려면 ELLCC 프로젝트 웹사이트를 방문해서 C 코드를 브라우저에서 바로 LLVM IR로 변환하는 라이브 데모를 보면 된다.
 

프로그래밍 언어에서 LLVM을 사용하는 방법

LLVM의 가장 일반적인 사용 사례는 언어용 AOT(Ahead-Of-Time) 컴파일러다. 예를 들어 클랭 프로젝트는 C와 C++를 네이티브 바이너리로 AOT 컴파일한다. 그러나 LLVM은 다른 것도 가능하게 해준다.
 

LLVM을 사용한 JIT 컴파일

사전에 코드를 컴파일하는 것이 아니라 코드를 런타임에 즉석 생성해야 하는 경우가 있다. 예를 들어 줄리아(Julia) 언어는 빠르게 실행되어 REPL(Read-Eval-Print Loop) 또는 대화형 프롬프트를 통해 사용자와 상호작용해야 하므로 코드를 JIT 컴파일한다.
 
파이썬용 수학 가속 패키지인 넘바(Numba)는 일부 파이썬 함수를 기계 코드로 JIT 컴파일한다. 넘바로 데코레이션된 코드를 AOT 컴파일할 수도 있지만 (줄리아와 마찬가지로) 파이썬은 인터프리트 언어라는 특성을 기반으로 빠른 개발 속도를 제공한다. JIT 컴파일을 사용해서 이러한 코드를 생성하면 AOT 컴파일에 비해 파이썬의 인터랙티브한 워크플로우를 더 잘 보완할 수 있다.
 
LLVM을 JIT로 사용하는 새로운 방법에 대한 실험도 이뤄지고 있다. 예를 들어 포스트그리SQL 쿼리를 컴파일에서 성능을 5배 높이는 실험 등이 있다.
 

LLVM을 사용한 자동 코드 최적화

LLVM은 IR을 네이티브 기계 코드로 컴파일하는 기능만 하는 것이 아니다. 프로그램에 따른 방법으로 링크 프로세스 전반에서 세밀하게 코드를 최적화할 수도 있다. 최적화는 함수 인라인, 죽은 코드 제거(사용되지 않는 형식 선언과 함수 인수 포함), 루프 풀기 등 상당히 공격적이다.
 
역시나 강점은 이러한 모든 부분을 직접 구현할 필요가 없다는 데 있다. LLVM이 자동으로 처리할 수 있다. 또는 필요에 따라 최적화를 끄도록 설정할 수도 있다. 예를 들어 약간의 성능 저하를 대가로 더 작은 바이너리를 원한다면 컴파일러 프론트 엔드에서 LLVM에 루프 풀기를 비활성화하도록 지정할 수 있다.
 

LLVM을 사용한 영역별 언어

LLVM은 많은 범용 언어를 위한 컴파일러 제작에 사용돼 왔지만 고도의 수직성, 즉 한 가지 문제 영역에 특화된 언어를 생성하는 데도 유용하다. 어떤 면에서는 이 부분이 LLVM이 가장 빛을 발하는 부분이다. 이러한 언어를 만드는 과정의 단조롭고 힘든 일을 상당 부분 제거해 원활한 진행을 보조해주기 때문이다.
 
예를 들어 Emscripten 프로젝트는 LLVM IR 코드를 가져와 이를 자바스크립트로 변환한다. 이렇게 되면 이론적으로는 LLVM 백엔드가 있는 어떤 언어든 브라우저 내에서 실행되는 코드를 내보낼 수 있다. 장기적인 계획은 웹어셈블리(WebAssembly)를 생산할 수 있는 LLVM 기반 백엔드를 만드는 것이지만 Emscripten은 LLVM이 얼마나 유연해질 수 있는지를 보여주는 좋은 예다.
 
LLVM을 사용해서 기존 언어에 영역별 확장을 추가할 수도 있다. 엔비디아는 LLVM을 사용해서 엔비디아 CUDA 컴파일러(Nvidia CUDA Compiler)를 만들었다. 이 컴파일러는 각 언어에서 함께 제공되는 라이브러리를 통해 호출되는 방식(느림) 대신, 개발자가 생성하는 네이티브 코드의 일부로 컴파일되는(빠름) CUDA를 위한 네이티브 지원을 추가할 수 있게 해준다.
 
LLVM이 영역별 언어에서 성공적인 결과를 내자 LLVM 내에서 이와 관련해 발생하는 문제에 대처하기 위한 새로운 프로젝트가 생겨났다. 가장 큰 문제는 일부 DSL은 프론트 엔드에서의 힘든 작업 없이 LLVM IR로 변환하기가 어렵다는 것이다. 한 가지 개발 중인 해결책은 다수준 중간 표현(Multi-Level Intermediate Representation – MLIR) 프로젝트다.
 
MLIR은 복잡한 데이터 구조와 연산을 표현하는 편리한 방법을 제공한다. 이 표현을 LLVM IR로 자동으로 변환할 수 있다. 예를 들어 텐서플로우(TensorFlow) 머신 러닝 프레임워크는 복잡한 데이터 흐름 그래프 연산의 상당수를 MLIR을 사용해 네이티브 코드로 효율적으로 컴파일할 수 있다.
 

다양한 언어에서 LLVM을 사용한 작업

LLVM을 다루는 보편적인 방법은 자신에게 익숙한 언어를 통하는 방법이다(물론 그 언어가 LLVM의 라이브러리를 지원해야 함).
 
두 가지 일반적으로 선택되는 언어는 C와 C++다. 많은 LLVM 개발자가 다음과 같은 여러 가지 이유로 이 두 언어 중 하나를 기본적으로 사용한다.

•    LLVM 자체가 C++로 만들어졌다.
•    LLVM의 API는 C와 C++ 구현으로 제공된다.
•    대부분의 언어 개발이 대체로 C/C++를 바탕으로 한다.
 
이 두 언어 외의 선택안도 있다. 많은 언어가 C 라이브러리를 네이티브로 호출할 수 있으므로 이론적으로는 이러한 모든 언어에서 LLVM 개발을 수행할 수 있다. 그러나 LLVM의 API를 매끄럽게 감싼 실제 라이브러리가 언어에 있는 편이 도움이 된다. 다행히 C#/닷넷/모노, 러스트, 하스켈, OCAML, Node.js, 고, 파이썬을 비롯한 많은 언어와 언어 런타임이 이러한 라이브러리를 갖고 있다.
 
한 가지 주의점은 LLVM에 대한 언어 바인딩 중에서 비교적 완성도가 떨어지는 경우가 있다는 것이다. 예를 들어 파이썬의 경우 많은 선택안이 있지만 완성도와 유용성은 제각각이다.

•    llvmlite – 넘바를 만든 팀이 개발했으며 파이썬에서 LLVM을 다루기 위한 유력한 방법으로 부상했다. 넘바 프로젝트의 필요에 따라 LLVM 기능의 하위 집합만 구현하지만 그 하위 집합이 LLVM 사용자들에게 필요한 기능을 대부분 제공한다. (일반적으로 파이썬에서 LLVM을 다루는 데 있어 llvmlite가 최선의 선택이다.)

•    LLVM 프로젝트는 LLVM의 C API에 대한 자체 바인딩 집합을 유지하고 있지만 현재는 유지관리가 되지 않고 있다.

•    llvmpy – LLVM용으로 널리 사용된 첫 파이썬 바인딩으로, 2015년부터 유지보수가 중단됐다. 모든 소프트웨어 프로젝트에서 아쉬운 일이지만, LLVM의 각 에디션마다 적용되는 많은 변화를 감안할 때 특히 LLVM 작업 측면에서 아쉽다. 

•    llvmcpy – C 라이브러리를 위한 파이썬 바인딩을 자동화된 방식으로 최신 상태로 유지하고 파이썬의 네이티브 구문을 사용해서 액세스할 수 있도록 하는 데 목표를 둔다. llvmcpy는 아직 초기 단계지만 이미 LLVM API로 몇 가지 기본적인 작업이 가능하다.

LLVM 라이브러리를 사용해서 언어를 만드는 방법이 궁금하다면 LLVM을 만든 당사자들이 제공하는 자습서를 참고하라. C++ 또는 OCAML을 사용해서 칼레이도스코프(Kaleidoscope)라는 간단한 언어를 만드는 과정을 단계별로 안내한다. 자습서는 다음과 같은 다른 언어로도 이식됐다.

•    하스켈: 원본 자습서를 그대로 이식했다.
•    파이썬: 이식 중 하나는 자습서를 거의 그대로 따르고, 다른 하나는 인터랙티브한 명령줄을 사용한 초월 이식이다. 두 가지 모두 llvmlite를 LLVM 바인딩으로 사용한다.
•    러스트와 스위프트: 존재 자체부터 LLVM의 도움을 받은 만큼 이 두 언어의 이식은 당연하다고 볼 수 있다.
 
마지막으로, 다른 언어(인간의 언어)로도 제공된다. 자습서는 원래의 C++와 파이썬을 사용해서 중국어로 번역됐다.
 

LLVM에 없는 기능

LLVM은 다양한 기능을 제공하지만 LLVM에 없는 기능도 알아두는 것이 좋다.
 
예를 들어 LLVM은 언어의 문법을 파싱하지 않는다. lex/yacc, flex/bison, Lark, ANTLR 등 이 작업을 하는 툴은 이미 많다. 어차피 파싱은 컴파일에서 분리되는 추세이므로 LLVM이 이 부분에 신경을 쓰지 않는 것을 두고 의외라고 하기는 어렵다.
 
또한 LLVM은 특정 언어를 둘러싼 더 넓은 범위의 소프트웨어 문화에 직접적으로 대응하지 않는다. 따라서 컴파일러의 바이너리 설치와 설치의 패키지 관리, 툴체인 업데이트는 모두 직접 해야 한다.

마지막으로, 가장 중요한 점은 LLVM이 프리미티브를 제공하지 않는 일반적인 언어 요소가 아직 있다는 것이다. 많은 언어에는 메모리를 관리하는 주된 방법, 또는 RAII(C++와 러스트에서 사용)와 같은 전략의 부가적 요소로서 가비지 수집된 메모리를 관리하는 방식이 있다. LLVM은 가비지 수집기 메커니즘을 제공하지 않지만, 가비지 수집기를 더 쉽게 쓸 수 있게 해주는 메타데이터를 코드에 표시할 수 있도록 함으로써 가비지 수집을 구현하기 위한 툴은 제공한다.
 
그러나 LLVM이 가비지 수집을 구현하기 위한 네이티브 메커니즘을 추가할 가능성도 배제할 수는 없다. LLVM은 빠른 속도로 개발되면서 약 6개월 단위로 주 릴리스가 나오고 있다. 또한 많은 현행 언어가 LLVM을 개발 프로세스의 중심에 두고 있으므로 앞으로 그 속도는 더 빨라질 가능성이 높다. editor@itworld.co.kr 

회사명 : 한국IDG | 제호: ITWorld | 주소 : 서울시 중구 세종대로 23, 4층 우)04512
| 등록번호 : 서울 아00743 등록발행일자 : 2009년 01월 19일

발행인 : 박형미 | 편집인 : 박재곤 | 청소년보호책임자 : 한정규
| 사업자 등록번호 : 214-87-22467 Tel : 02-558-6950

Copyright © 2024 International Data Group. All rights reserved.