개발자

“프로그래밍 언어 개발의 감초” LLVM의 이해와 활용 방법

Serdar Yegulalp | InfoWorld 2023.08.23
현재 개발 환경 분야에서는 매일 새로운 언어와 기존 언어에 대한 개선이 쏟아지고 있다. 모질라의 러스트, 애플의 스위프트, 젯브레인스의 코틀린, 그리고 실험적 파이썬 변형인 모조(Mojo) 등이 대표적이다. 모두 개발자에게 속도, 안전성, 편의성, 이식성 및 성능에 대한 폭넓은 선택권을 제공한다. 이런 변화 중 상당수가 언어, 특히 컴파일러를 구축하기 위한 새로운 툴이다. 컴파일러 중에서도 대표 주자인 LLVM은 스위프트 언어를 만든 일리노이 대학의 크리스 래트너가 처음 개발한 오픈소스 프로젝트다. 
 
ⓒ Getty Image Bank

LLVM을 사용하면 새로운 언어를 만드는 것뿐만 아니라 기존 언어 개발을 강화하는 작업도 더 쉽게 할 수 있다. LLVM은 새로운 언어를 만드는 과정에서 힘들기만 하고 티는 나지 않는 많은 부분, 예컨대 출력된 코드를 여러 플랫폼과 아키텍처로 이식하기, 벡터화와 같은 아키텍처별 최적화 생성하기, 예외와 같은 일반적인 언어 메타포를 처리하기 위한 코드 작성하기 등의 작업을 자동화한다. 자유로운 라이선스 덕분에 소프트웨어 구성요소로 원하는 대로 재사용하거나 서비스로 배포할 수 있다. 

LLVM을 사용하는 언어는 이미 우리에게 익숙한 것이 많다. 애플의 스위프트 언어는 LLVM을 컴파일러 프레임워크로 사용하고, 러스트는 러스트 툴체인의 핵심 구성요소로 사용한다. C/C++ 컴파일러인 클랭을 포함한 많은 컴파일러가 LLVM을 사용한다. 닷넷 구현인 모노에는 LLVM 백엔드를 사용해서 네이티브 코드로 컴파일하는 옵션이 있다. JVM 언어인 코틀린은 LLVM을 이용해서 머신 네이티브 코드로 컴파일하는 코틀린/네이티브라는 컴파일러 기술을 제공한다. 
 

LLVM이란

기본적으로 LLVM은 프로그래밍 방식으로 머신 네이티브 코드를 생성하는 라이브러리다. 개발자는 API를 사용해 중간 표현(intermediate representation, IR)이라는 형식의 명령어를 생성한다. 그 후 이 IR을 독립형 바이너리로 컴파일하거나, 언어의 인터프리터 또는 런타임과 같은 다른 프로그램 컨텍스트에서 실행되도록 코드에 대해 JIT(Just-In-Time) 컴파일을 수행할 수 있다. 

LLVM의 API는 프로그래밍 언어의 많은 일반적인 구조와 패턴을 개발하는 프리미티브를 제공한다. 예를 들어 거의 모든 언어에는 함수와 전역 변수 개념이 있고 많은 언어에 코루틴과 C 외부 함수 인터페이스가 있다. LLVM은 함수와 전역 변수를 IR의 표준 요소로 갖고 있으며 코루틴을 만들고 C 라이브러리와 접속하기 위한 메타포가 있다. 이처럼 이미 있는 요소를 다시 만드는 데 시간을 낭비할 필요 없이 LLVM의 구현을 사용하고 언어에서 더 많은 관심이 필요한 부분에 집중할 수 있다. 
 
왼쪽은 간단한 C 프로그램이고 오른쪽은 같은 코드를 클랭 컴파일러를 이용해 LLVM IR로 변환한 것이다. ⓒ IDG
 

가장 큰 이점은 이식성

LLVM을 이해하기 위해 C 프로그래밍 언어에 비유해 보자. C는 이식 가능한 고수준 어셈블리어로 유명하다. C에 시스템 하드웨어와 밀접하게 매핑할 수 있는 구조가 있고, 거의 모든 시스템 아키텍처로 이식됐기 때문이다. 그러나 C가 가진 이식 가능한 어셈블리어로써의 유용함에는 한계가 있다. 애초에 그 목적을 위해 설계된 언어가 아니기 때문이다. 

반면 LLVM의 IR은 처음부터 이식 가능한 어셈블리로 시작됐다. 이 이식성을 달성하는 한 가지 방법은 특정 머신 아키텍처에 종속되지 않는 독립적인 프리미티브를 제공하는 것이다. 예를 들어 정수 형식은 32비트 또는 64비트 등 기반 하드웨어의 최대 비트 폭에 제한되지 않는다. 128비트 정수와 같이 필요한 만큼 많은 비트를 사용해 프리미티브 정수 형식을 만들 수 있다. 또한 특정 프로세서의 명령어 집합에 맞게 출력을 가공하는 데 신경 쓸 필요도 없다. LLVM이 알아서 처리한다.

LLVM의 이런 아키텍처 중립적 설계 덕분에 현재와 미래의 모든 종류의 하드웨어를 쉽게 지원한다. 예를 들어 IBM은 z/OS, 파워 기반 리눅스(IBM의 MASS 벡터화 라이브러리 지원 포함), 그리고 LLVM의 C, C++ 및 포트란 프로젝트의 AIX 아키텍처를 지원하기 위한 코드를 기여했다. LLVM IR의 라이브 예제를 보려면 갓볼트 컴파일러 익스플로러(Godbolt Compiler Explorer) 사이트를 방문해서 C 또는 C++를 LLVM IR로 변환한다. 클랭을 컴파일러로 선택한 다음 새로 추가(Add new)에서 LLVM IR을 선택한다. 그러면 탭이 열리고, 제공된 C/C++ 언어에서 생성된 LLVM IR 코드가 표시된다. 
 

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

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

LLVM을 사용한 JIT 컴파일 
상황에 따라 사전에 컴파일하지 않고 런타임에 즉석에서 코드를 생성해야 할 수 있다. 예를 들어 줄리아(Julia) 언어는 빠르게 실행되면서 REPL(읽기-평가-인쇄 루프) 또는 인터랙티브 프롬프트를 통해 사용자와 상호작용해야 하므로 코드를 JIT 컴파일한다. 

파이썬용 수학 가속 패키지인 넘바(Numba)는 선택된 파이썬 함수를 머신 코드로 JIT 컴파일한다. 넘바로 데코레이션된 코드를 사전 컴파일하기도 하지만, 줄리아와 마찬가지로 파이썬 역시 인터프리터 언어 장점을 발휘해 빠른 개발을 제공한다. JIT 컴파일을 사용해 이러한 코드를 생산하는 방식은 AOT 컴파일에 비해 파이썬의 인터랙티브 워크플로우를 더 잘 보완한다. 그 외에 LLVM을 포스트그래SQL 쿼리 컴파일 등 JIT 컴파일러로 사용해 최대 5배까지 성능을 높이는 새로운 방법에 대한 실험도 진행되고 있다.
 
넘바는 LLVM을 이용해 수학 코드를 JIT 컴파일하고 더 빠르게 실행한다. JIT 가속 sum2d 함수는 일반적인 파이썬 코드보다 139배 더 빠르다. ⓒ IDG

LLVM을 사용한 자동 코드 최적화 
LLVM은 단순히 IR을 네이티브 머신 코드로 컴파일하는 것이 전부가 아니다. 프로그래밍을 통해 링크 프로세스 전반에 걸쳐 높은 수준으로 세분화해 코드를 최적화한다. 최적화의 경우 함수 인라이닝, 불필요한 코드 제거(사용되지 않는 형식 선언 및 함수 인수 포함), 루프 풀기를 포함해 상당히 공격적인 최적화가 가능하다. 여기서도 장점은 직접 구현할 필요가 없다는 것이다. LLVM이 알아서 이러한 작업을 처리하며, 필요에 따라 해당 기능을 끄거나 켤 수 있다. 예를 들어 약간의 성능 하락을 감수하는 대신 더 작은 바이너리를 원한다면 컴파일러 프론트 엔드에서 LLVM에 루프 풀기를 비활성화하도록 설정할 수 있다.

LLVM을 사용한 도메인 특화 언어  
LLVM은 많은 범용 언어용 컴파일러에 사용됐지만 고도로 수직적이거나 한 문제 영역에 특화된 언어를 만드는 데도 유용하다. 어떤 면에서는 LLVM이 가장 빛을 발하는 부분이다. 이런 언어를 만들고 잘 동작하도록 하는 많은 고된 작업을 없애 주기 때문이다. 예를 들어 엠스크립튼(Emscripten) 프로젝트는 LLVM IR 코드를 자바스크립트로 변환한다. 이를 통해 이론적으로는 LLVM 백엔드를 사용하는 어느 언어에서나 브라우저에서 실행 가능한 코드를 내보낼 수 있다. 이 작업의 주요 성과 중 하나는 웹어셈블리를 생성해 러스트와 같은 언어가 WASM을 타겟으로 직접 컴파일할 수 있게 해주는 LLVM 기반 백엔드다. 

LLVM을 사용하는 또 다른 방법은 도메인 특화 확장을 기존 언어에 추가하는 것이다. 엔비디아는 LLVM을 사용해서 엔비디아 CUDA 컴파일러를 만들었다. 이를 통해 언어에서 함께 제공되는 라이브러리를 통해 호출되는 대신(느림) 생성 중인 네이티브 코드의 일부로 컴파일하는(빠름) CUDA 네이티브 지원을 추가할 수 있다. 

LLVM이 도메인 특화 언어에서 성공을 거두면서 LLVM 내에 여기서 발생하는 문제를 해결하기 위한 새로운 여러 프로젝트가 출범했다. 이때 가장 큰 문제는 일부 DSL을 LLVM IR로 변환하기 위해 프론트 엔드에서 많은 노력이 필요하다는 점이다. 현재 개발 중인 한 가지 해결책은 다중 단계 중간 표현(Multi-Level Intermediate Representation, MLIR) 프로젝트다. MLIR은 복잡한 데이터 구조와 연산을 편리하게 표현하는 방법을 제공하며 이 표현을 LLVM IR로 자동 변환한다. 예를 들어 텐서플로우 머신러닝 프레임워크의 많은 복잡한 데이터플로우 그래프 연산을 MLIR을 사용해 네이티브 코드로 효율적으로 컴파일할 수 있다. 
 

LLVM을 사용하는 방법 

LLVM을 사용하는 일반적인 방법은 LLVM 라이브러리를 지원하는 언어로 코딩하는 것이다. 일반적으로 C와 C++가 있다. 많은 LLVM 개발자가 이 2가지 언어 중 하나를 기본적으로 사용하는데, 다음과 같은 이유 때문이다.
 
  • LLVM 자체가 C++로 작성됐다. 
  • LLVM의 API가 C와 C++ 버전으로 제공된다. 
  • 대부분 언어 개발은 대체로 C/C++를 기반으로 한다. 

물론 이 두 언어만 선택할 수 있는 것은 아니다. 이론적으로는 C 라이브러리를 네이티브로 호출할 수 있는 많은 언어에서 LLVM 개발을 수행할 수 있다. 그러나 LLVM의 API를 매끄럽게 래핑하는 실제 라이브러리가 언어에 있는 편이 도움이 된다. 다행히 C#/닷넷/모노, 러스트, 하스켈, OCAML, Node.js, , 파이썬을 포함한 많은 언어와 언어 런타임에 이런 라이브러리가 있다. 한 가지 주의할 부분은 LLVM에 대한 언어 바인딩 중 일부는 완성도가 떨어진다는 점이다. 예를 들어 파이썬에서는 그동안 많은 선택 옵션이 등장했지만 완성도와 유용성은 제각각이다. 그 중 안정적이고 강력한 것을 소개하면 다음과 같다.
 
  • llvmlite는 넘바를 만든 팀이 개발했으며, 현재 파이썬에서 LLVM을 다루는 보편적인 방법으로 부상했다. 넘바 프로젝트에 필요한 LLVM 기능의 일부만 구현하지만 그 일부가 대다수 LLVM 사용자에게 필요한 것들이다. llvmlite는 파이썬에서 LLVM을 다루는 최선의 선택지다.
  • LLVM 프로젝트 자체적으로 LLVM의 C API에 대한 바인딩이 있지만 현재 유지보수가 되지 않고 있다. 
  • llvmpy는 광범위하게 사용된 LLVM을 위한 파이썬 바인딩으로 유명해졌지만, 2015년부터 유지보수가 중단됐다. 유지보수 중단은 모든 소프트웨어 프로젝트에서 좋지 않지만 새 버전에서 도입된 여러 가지 변화를 감안하면 LLVM과 관련해서는 특히 안타깝다.
  • llvmcpy는 C 라이브러리용 파이썬 바인딩을 자동으로 최신 상태로 유지하고 파이썬의 네이티브 관용구를 사용해 액세스할 수 있도록 하는 데 목표를 둔다. llvmcpy는 현재 LLVM API를 사용해 몇 가지 기초적인 작업을 할 수 있지만 2019년 이후 업데이트가 되지 않았다. 

LLVM 라이브러리를 사용해서 언어를 구축하는 방법에 관심이 있다면 LLVM 제작자들이 준비해 둔 자습서를 참고하면 된다. 자습서는 C++ 또는 OCAML을 사용하며 칼레이도스코프(Kaleidoscope)라는 간단한 언어를 만드는 과정을 단계별로 안내한다. 자습서는 다음과 같은 다른 언어로도 이식됐다. 
 
  • 하스켈 : 원본 자습서를 그대로 이식 
  • 파이썬 : 하나는 원본 자습서를 거의 그대로 따르고, 다른 하나는 인터랙티브 명령줄을 사용해서 더 확장해 다시 만들었다. 둘 다 llvmlite를 LLVM 바인딩으로 사용한다. 
  • 러스트스위프트 : 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
Sponsored

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

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

Copyright © 2024 International Data Group. All rights reserved.