원문 캡처 · bkaradzic.github.io
원문 캡처 · bkaradzic.github.io

셰이더 컴파일러를 오랫동안 관리해 온 bgfx 제작자 브라이언 카라지치(Brian Karadzic)가 FXC, DXC, Metal compiler, Khronos 스택, Tint, Naga, NIR, Slang을 기능과 크기, 컴파일 속도로 비교했다. 조사에서는 Naga가 가장 빠르고 작으며 설정하기 쉽고 공개 IR을 제공했지만, 최적화기가 없고 Rust로 작성돼 C++ 프로젝트에 넣기 어렵다는 한계도 드러났다.

이상적인 셰이더 컴파일러의 조건

카라지치는 셰이더 언어 자체보다 컴파일러의 입력과 출력, 각 단계를 얼마나 제어할 수 있는지가 중요하다고 본다. 오프라인·임베디드 사용을 전제로, 셰이더를 실행 중에 컴파일할 수 없는 플랫폼, 최적화된 중간 표현(IR)을 배포하는 경우, 실행 파일 크기를 줄여야 하는 경우, 특정 단계를 끼워 넣거나 건너뛰어야 하는 경우를 고려해 이상적인 컴파일러의 조건 10가지를 정했다.

  • 텍스트를 입력받아 다시 텍스트를 출력하고, 필요하면 최적화할 수 있어야 한다. 드라이버의 최적화 성능이 좋지 않고 API가 소스 코드만 받는 상황에서, 셰이더를 배포하기 전에 기본 최적화를 할 수 있어야 한다. glsl-optimizer가 이 사례다.
  • NDA가 적용되는 게임 콘솔처럼 실행 중 코드 생성이 불가능한 플랫폼에서도, 마지막 단계를 독점 컴파일러로 교체할 수 있어야 한다. 플랫폼 컴파일러가 외부에 공개되지 않은 방식으로 바이너리에 서명할 수도 있으므로 마지막 단계는 블랙박스로 대체할 수 있어야 한다.
  • 컴파일러의 일부 또는 전체를 건너뛸 수 있어야 한다. 컴파일러가 지원하지 않는 기능을 급히 써야 할 때 별도 기능을 덧붙이지 않고 백엔드 컴파일러로 바로 넘길 수 있어야 한다.
  • 모든 단계에서 IR에 접근해 필요한 형태로 바꿀 수 있어야 한다.
  • 대상 플랫폼에 필요하지 않은 기능을 제거할 수 있어야 하며, 제거한 만큼 바이너리도 작아져야 한다.
  • 명령줄 도구에 main()을 호출하는 기능을 덧붙인 형태가 아니라 임베드할 수 있는 라이브러리여야 한다. 텍스트부터 셰이더 바이너리까지 처리하거나, 렉싱·파싱·최적화를 건너뛰고 IR을 대상 셰이더 바이너리로 바꾸는 제한된 형태로도 사용할 수 있어야 한다. 또한 컴파일러를 임베드했을 때 실행 파일 크기가 지나치게 커져서는 안 된다. 1MiB 실행 파일에 100MiB 라이브러리를 링크하면 실질적인 임베드라고 보기 어렵다.
  • IR을 직렬화할 수 있어야 한다. 최적화한 셰이더 IR을 배포하고 실행 중 특정 백엔드에 맞춰 변환하거나 출력할 수 있다. 모든 곳에서 가능한 방식은 아니지만, 가능하다면 유용하다.
  • 컴파일러 없이 실행 중 IR을 조립할 수 있어야 한다. 셰이더 생성이 단순하고 최적화할 이점이 없다면 IR을 직접 출력하고 백엔드만 링크해 바이너리 크기를 줄일 수 있어야 한다.
  • Windows, Linux, macOS 등 주요 데스크톱 운영체제에서 빌드할 수 있어야 한다.
  • 빠르게 동작해야 한다. 여러 셰이더를 동시에 컴파일할 때 재진입 가능하고 다중 스레드를 쓸 수 있어야 한다. 서로 독립적인 셰이더 컴파일을 순차 처리하도록 강제해서는 안 된다.

카라지치는 이 조건들이 특이하거나 구현 불가능한 요구가 아니라고 말한다. LLVM 프로젝트도 스스로를 모듈식이며 재사용 가능한 컴파일러와 도구 체계의 모음으로 설명한다.

비교 대상과 조건 충족 결과

비교 대상은 FXC, DXC, Metal compiler, Khronos 스택(Glslang·SPIRV-Tools·SPIRV-Cross), Dawn의 Tint, wgpu의 Naga, Mesa의 NIR, Slang이다. FXC는 HLSL을 Direct3D 11용 DXBC로, DXC는 HLSL을 Direct3D 12용 DXIL 또는 SPIR-V로 바꾼다. Metal compiler는 Metal Shading Language(MSL)를 AIR로 컴파일한다. Khronos 스택은 GLSL 또는 HLSL을 SPIR-V로 바꾸고 최적화한 뒤 GLSL, ESSL, HLSL, MSL로 되돌린다. Tint는 WGSL 또는 SPIR-V를 입력받아 WGSL, SPIR-V, HLSL, MSL, GLSL을 출력한다. Naga는 WGSL, SPIR-V, GLSL을 입력받고 같은 다섯 형식으로 출력한다. NIR은 Mesa 드라이버가 거치는 IR이며 SPIR-V·GLSL을 입력받고 최적화한 뒤 SPIR-V·DXIL·MSL을 출력한다. Slang은 Slang 또는 HLSL을 입력받아 SPIR-V, HLSL, MSL, WGSL, GLSL을 출력하며 DXIL은 DXC를 거친다.

아래 점수는 완전한 ‘예’를 1점, 조건이 붙은 항목을 일부 충족으로 계산했다.

조건FXCDXCMetalKhronos 스택TintNagaNIRSlang
텍스트 입력·출력아니요아니요아니요예예, 최적화기 없음예, 최적화기 없음GLSL 입력, MSL 출력, 드라이버 내부예
독점 백엔드 연결자체가 독점 백엔드자체가 독점 백엔드자체가 독점 백엔드예예예예예
일부 단계 생략아니요아니요일부예일부예예일부
어느 단계든 삽입아니요일부아니요일부일부예예아니요
설정 가능아니요아니요아니요일부예예일부아니요
라이브러리예예아니요예예Rust만소스 트리 내부에서만예
직렬화 가능한 IR아니요예예예일부예빌드별로 예예
실행 중 IR 조립아니요아니요아니요일부일부예예아니요
Windows·Linux·macOSWindows예Linux 제외예예예예예
빠르고 재진입 가능예컴파일러 인스턴스별로 예프로세스예예예예스레드별 전역 세션 하나
예 / 일부 충족2 / 13 / 21 / 27 / 35 / 58 / 26 / 45 / 2

FXC는 D3DCompile 함수 하나로 HLSL을 DXBC로 바꾸며, 사용자가 개입할 중간 단계가 없다. 폐쇄형이고 Windows에서만 동작하며 Shader Model 5.1에서 멈췄다. 조건 대부분을 충족하지 못하지만, 독점 백엔드를 마지막에 붙이는 두 번째 조건에는 해당한다.

DXC는 LLVM 3.7에 HLSL 프런트엔드를 붙인 구조다. 필요한 기능은 내부에 있지만 대부분 외부에서 접근할 수 없다. IDxcOptimizer로 이름을 지정해 패스를 실행하는 것이 사실상 유일한 제어 수단이다. SPIR-V 백엔드가 필요하든 아니든 DLL 하나가 18MB다. 다만 이 백엔드는 중요하다. Glslang의 HLSL 프런트엔드가 폐기될 예정이어서 Vulkan용 HLSL 컴파일은 DXC를 향하고 있다.

Metal compiler는 Clang을 기반으로 한다. 명령줄 도구 metal은 MSL을 LLVM 비트코드인 AIR로 컴파일할 뿐이며, 프로세스 안에서 쓰는 Metal 컴파일러는 운영체제 내부에만 있다. Apple은 macOS와 Windows용으로 배포하지만 Linux용은 제공하지 않는다.

Glslang·SPIRV-Tools·SPIRV-Cross를 합친 Khronos 스택은 텍스트 입출력 조건을 온전히 충족한다. 세 프로젝트를 조합했기 때문에 각 라이브러리 사이에서 처리를 멈추거나 자체 단계를 넣기 쉽다는 점에서도 유리하다. 반면 전체를 책임지는 단일 주체가 없어 프런트엔드가 백엔드와 별개로 폐기될 수 있다. HLSL에서는 최적화 도구가 선택 사항이 아니다. Glslang이 출력한 SPIR-V는 SPIRV-Tools가 합법화하기 전까지 유효하지 않을 수 있다.

Tint에는 IR, 빌더, 변환 단계가 있고, 독자와 출력기를 컴파일 시점에 선택하는 스위치가 있다. 따라서 IR 삽입·설정·실행 중 IR 조립 조건을 이론상 충족한다. 하지만 Dawn 내부의 C++ 구성요소로 안정성 보장은 없다. 공개 헤더 tint.h가 제공하는 기능은 Initialize, Shutdown, SpirvToWgsl이다. 설계상 최적화기는 없다. SPIR-V 독자는 출력을 직접 생성하지 않고 WGSL 추상 구문 트리(AST)를 거쳐 다시 변환한다.

Naga는 조건 목록에 가장 가깝다. 공개 데이터 구조인 naga::Module을 직접 만들고 순회·수정한 뒤 원하는 백엔드에 넘길 수 있다. 프런트엔드와 백엔드는 각각 컴파일에서 제외할 수 있고, 모듈은 직렬화할 수 있으며 프런트엔드·검증기·백엔드 호출도 분리돼 있다. 다만 최적화기와 C API가 없다. 출력은 유효하지만 최적화되지 않으므로 최적화하려면 별도 도구가 필요하다. Rust로 작성돼 C++에서 쓰려면 타입을 감싸는 C 래퍼를 직접 만들고 Naga가 자주 바뀔 때마다 이를 고쳐야 한다. 카라지치는 Rust 프로젝트에는 적합하지만 C++ 프로젝트에서는 사실상 쓰기 어렵다고 평가한다.

NIR은 모든 Mesa 드라이버와 Microsoft의 Direct3D 12 기반 OpenCL·OpenGL 계층이 사용하는 IR이다. 널리 쓰이고 요구 조건도 상당수 갖췄지만, 애플리케이션 드라이버가 아닌 그래픽 드라이버의 필요에 맞춰 만들어졌다. 패스는 임의 순서로 호출할 수 있는 C 함수이며, nir_builder로 IR을 직접 만들고 nir_serialize로 저장할 수 있고 실제 최적화기도 있다. Faith Ekstrand의 NIR 옹호 글은 설계 배경을 설명한다. SPIR-V·GLSL을 입력받고, 하드웨어 백엔드와 함께 SPIR-V·DXIL·MSL 출력기도 갖췄다. 하지만 공개 헤더·안정적인 API·정식 릴리스가 없고 직렬화 형식은 작성한 빌드에 종속된다. 출력기는 각각 특정 드라이버에 맞춰 작성됐다. MSL 출력기는 드라이버의 디스크립터 하향 변환 없이는 작동하지 않아 벤치마크에서 제외됐다.

Slang은 여러 대상을 지원하는 완성형 컴파일러로 설계됐다. 자체 프런트엔드와 IR, 패스, 출력기, API를 갖추고 모든 플랫폼을 지원하며 모듈을 직렬화된 IR로 저장한다. FXC·DXC 등 하위 컴파일러를 호출할 수 있고, 소스를 건드리지 않고 전달하는 통과 모드도 있어 독점 백엔드 연결을 중시한다. 반면 IR을 읽거나 수정하거나 직접 만드는 API는 없다. 모든 기능이 한 덩어리로 제공돼 slang-compiler.dll은 26MB이며 SPIR-V를 쓰려면 Glslang과 SPIRV-Tools 6MB도 더 필요하다.

벤치마크 방법

Naga 제작자가 2022년에 발표한 셰이더 변환 벤치마크는 프로세스 내부에서 측정하고 입출력 시간을 제외했으며, 모든 라운드에서 Naga가 10배 빠르다고 보고했다. 카라지치는 이 결과가 4년 전 자료라며 현재 버전을 다시 비교했다.

입력 데이터는 bgfx 예제의 버텍스·프래그먼트·컴퓨트 셰이더 368개다. 버텍스 120개, 프래그먼트 203개, 컴퓨트 45개로, 네 줄짜리 버텍스 셰이더부터 FSR과 ASSAO까지 포함한다. 각 셰이더는 비교 대상 언어 형식으로 준비했다. HLSL은 31만 6천 줄이며, 각 셰이더가 대부분 호출하지 않는 공통 헬퍼 함수 1천 줄을 포함해 컴파일러가 셰이더마다 다시 파싱한다. 최적화된 SPIR-V는 1.9MB, WGSL은 4만 1천 줄, MSL은 3만 7천 줄이다.

Glslang, SPIRV-Tools, SPIRV-Cross, Tint는 하나의 C++ 실행 파일에 정적으로 링크했다. FXC와 DXC는 d3dcompiler_47.dll, dxcompiler.dll을 불러왔다. Naga는 별도 Rust 실행 파일로, Slang은 배포 패키지 DLL로 측정했다. 셰이더를 먼저 메모리에 올리고 각 컴파일을 10회 실행해 가장 빠른 시간을 선택한 뒤 전체 셰이더의 시간을 합산했다. Slang은 5회 실행했다. 모든 측정은 단일 스레드다. Metal은 라이브러리가 없어 셰이더마다 프로세스를 실행했으며 3회 중 가장 빠른 시간을 기록했다. 프로세스 시작 시간은 따로 측정했다.

버전은 Glslang 16.1.0, SPIRV-Tools v2025.5, Dawn 커밋 4b91cc492에 고정된 Tint, 2026년 8월판 SPIRV-Cross, Naga 30.0.1, Slang v2026.19, DXC 1.10.2605, FXC 10.0.26100, Metal 32023.850, Mesa 커밋 e2ab668이다. FXC와 DXC는 -O3로 실행했다. SPIRV-Tools는 합법화 및 성능 패스를 적용했고 Slang은 기본 설정을 썼다. Tint는 명령줄 도구와 같은 방식으로 실행했다. Mesa는 spirv_to_nir를 거친 뒤 Zink의 패스와 SPIR-V 출력기, 또는 spirv_to_dxil을 사용했다. Zink는 Vulkan 위에서 OpenGL을 구현하는 Mesa 드라이버다. 측정 환경은 MSVC 릴리스 빌드, Ryzen 9 7950X, Windows 10이다.

소스에서 드라이버 입력까지

아래 값은 368개 셰이더를 각각 해당 API 드라이버가 받을 수 있는 형식으로 컴파일한 결과다. 각 셰이더를 따로 컴파일하고 10회 중 가장 빠른 시간을 골라 합산했다.

경로변환컴파일 수전체 시간셰이더당 중앙값최장 셰이더
NagaWGSL → SPIR-V3680.22초85µs39ms
TintWGSL → SPIR-V3680.64초408µs94ms
Glslang+SPIRV-ToolsHLSL → SPIR-V3684.96초3.4ms915ms
DXCHLSL → DXIL3685.99초11.3ms269ms
DXCHLSL → SPIR-V3668.56초10.6ms1053ms
SlangHLSL → WGSL36013.53초32.7ms102ms
SlangHLSL → HLSL36813.53초32.5ms100ms
SlangHLSL → MSL36613.68초32.9ms100ms
SlangHLSL → GLSL36813.71초32.4ms104ms
SlangHLSL → SPIR-V36814.70초33.4ms183ms
FXCHLSL → DXBC36815.43초4.8ms4088ms
MetalMSL → AIR36822.37초56.5ms325ms

Naga와 Tint는 HLSL을 읽지 않고 최적화도 하지 않으므로, WGSL을 받아 변환하는 별도의 비교군이다. 출력은 SPIRV-Tools의 후처리 없이도 쓸 수 있다. Tint가 출력한 모듈 368개는 모두 검증을 통과했고, Naga는 366개가 통과했다. 나머지 둘은 워크그룹 배열의 레이아웃 장식 문제로 실패했다. 전체 데이터에서 드라이버에 넣을 수 있는 SPIR-V를 만드는 데 Naga는 0.22초, Tint는 0.64초가 걸렸다. 나머지는 5초 이상이었다.

Glslang은 빠르지만 SPIRV-Tools는 그렇지 않으며, Glslang만으로는 충분하지 않다. HLSL 31만 6천 줄을 파싱해 SPIR-V를 출력하는 데 Glslang은 0.93초, 셰이더당 2.1ms가 걸렸다. 하지만 Glslang 도움말도 -Od disables optimization; may cause illegal SPIR-V for HLSL이라고 경고한다. 실제로 SPIRV-Tools가 합법화하기 전에는 368개 중 173개가 검증에 실패했고, 합법화한 뒤에는 모두 통과했다. 두 도구는 한 쌍으로 봐야 하며 전체에 4.96초가 걸렸다. 합법화와 최적화가 그중 81%를 차지한다. 전체 Khronos 스택으로 HLSL을 MSL로 바꾸는 데는 5.1초가 걸렸고, 이때도 최적화기가 79%를 차지했다.

FXC는 셰이더에 따라 차이가 컸다. 셰이더당 중앙값은 4.8ms로 DXC의 절반보다 짧았지만 전체 시간은 DXC보다 2.5배 길었다. 한 예제의 셰이더 3개가 각각 4.1초, 2.9초, 1.6초를 차지해 나머지 365개를 모두 합친 시간보다 오래 걸렸다. 같은 예제는 DXC, SPIRV-Tools, SPIRV-Cross, Tint에서도 가장 느린 셰이더를 포함했지만, 이들 도구의 최악 사례는 대체로 1초 안팎이었다. 카라지치는 이를 Direct3D 11 게임을 출시한 개발자들이 기억할 FXC의 모습이라고 표현했다.

DXC는 LLVM 기반의 특성이 속도에서도 드러났다고 카라지치는 평가했다. 가장 빠른 경우에도 11ms가 걸렸으며, Glslang과 SPIRV-Tools가 함께 3ms에 끝낸 셰이더였다. HLSL을 최적화된 SPIR-V로 바꾸는 같은 작업에서 DXC는 8.6초, Glslang과 최적화기 조합은 5.0초가 걸렸다. DXC는 컴퓨트 셰이더 두 개를 SPIR-V로 컴파일하지 못했다. 반면 DXIL로는 문제없이 컴파일했고 Glslang은 SPIR-V로 컴파일했다.

Slang은 HLSL에서 SPIR-V로 가는 가장 느린 경로였다. Khronos 스택보다 3배, DXC보다 1.7배 느렸다. 출력 대상과 무관하게 HLSL을 다시 출력하는 데도 SPIR-V 출력과 같은 셰이더당 33ms가량 걸렸다. 시간의 대부분은 프런트엔드에 들었다. 한 줄짜리 버텍스 셰이더는 0.5ms에 처리했으므로, 셰이더마다 붙은 헬퍼 함수 1천 줄을 호출 여부와 무관하게 검사하는 데 약 30ms가 든 셈이다. 반면 최악의 셰이더도 183ms로, FXC·DXC·SPIRV-Tools에서 각각 1초 이상 걸린 사례보다 편차가 작았다.

Slang은 HLSL 컴파일러보다 문법을 엄격하게 적용했다. 원본 셰이더 368개는 하나도 그대로 컴파일되지 않아 HLSL을 수정했다. FXC, DXC, Glslang은 받아들이지만 Slang이 거부한 사례는 다음과 같다.

  • 샘플러와 텍스처를 담은 전역 static 구조체를 “global variable cannot have opaque type” 오류로 거부했다. 166개 셰이더에서 나타났으며, 구조체를 사용하는 곳에서 매크로로 만드는 방식으로 피했다.
  • 헬퍼 함수에 전달한 텍셀 오프셋을 SampleLevel이나 Load에서 쓰려면 constexpr여야 했다. 호출하지 않는 함수여도 해당 헬퍼를 포함한 셰이더를 거부했다.
  • min16float 및 관련 형식을 지원하지 않았다.
  • 한 함수에 for (int k ...) 루프가 두 개 있으면 k가 모호하다고 판단했다.
  • WGSL 출력에서는 프래그먼트 출력을 구조체로 반환해야 했다. 진입점의 out float4 color : SV_TARGET0 매개변수는 “system value semantic not supported” 오류를 냈다.

이 다섯 가지 수정을 거친 뒤 Slang은 368개를 SPIR-V·GLSL·HLSL로, 366개를 MSL로, 360개를 WGSL로 컴파일했다. 남은 실패는 WGSL의 InterlockedAdd처럼 대상 언어가 지원하지 않는 기능 때문이었다.

Metal은 프로세스로 실행된다. metal을 시작해 아무 작업도 하지 않아도 10ms가 걸린다. 시작 시간을 빼도 셰이더당 47ms, 전체 18.7초다. 입력은 3만 7천 줄의 기계 생성 MSL로, 다른 컴파일러가 처리한 HLSL 31만 6천 줄보다 훨씬 적었다.

SPIR-V 입력에서 최적화 후 출력까지

이 측정에서는 Glslang이 최적화 없이 만든 SPIR-V를 받아 최적화한 뒤 다시 SPIR-V로 출력하는 NIR과 SPIRV-Tools를 비교했다. Naga와 Tint는 비교 기준을 보여주기 위해 넣었다. 이들은 최적화된 데이터셋을 변환할 뿐 최적화하지 않는다.

경로입력컴파일 수전체 시간셰이더당 중앙값최장 셰이더
Naga최적화된 SPIR-V3580.03초36µs2ms
Tint최적화된 SPIR-V3590.97초720µs128ms
Mesa, NIR최적화되지 않은 SPIR-V3683.80초169µs1111ms
SPIRV-Tools최적화되지 않은 SPIR-V3684.03초1.23ms909ms
Mesa, NIR → DXIL최적화되지 않은 SPIR-V3667.12초472µs2160ms

NIR 최적화기는 셰이더당 비용이 SPIRV-Tools의 7분의 1이지만, 전체 시간은 비슷했다. 중앙값 셰이더는 NIR에서 169µs, SPIRV-Tools에서 1.23ms를 썼다. SPIR-V를 읽는 데 Mesa는 41ms, 출력하는 데 40ms가 걸렸고 나머지는 하향 변환과 최적화 반복에 들었다. 다만 전체 3.80초 중 3.55초는 한 예제 54-s2h의 셰이더 20개가 차지했다. 해당 셰이더에서 SPIRV-Tools는 2.79초가 걸렸다. 나머지 348개에서는 NIR이 0.17초, SPIRV-Tools가 1.24초였다.

HLSL에서 시작하면 Glslang과 NIR 조합은 최적화 단계만 SPIRV-Tools에서 Mesa로 바꾼 Khronos 파이프라인이다. 같은 HLSL과 프런트엔드를 사용했을 때 SPIR-V 출력은 4.74초로 SPIRV-Tools를 쓴 4.96초와 비슷했다. DXIL 출력은 8.04초로, HLSL에서 직접 변환한 DXC의 5.99초보다 길었다.

NIR 출력은 드라이버를 위한 형태다. Zink는 NIR에서 모든 벡터를 스칼라화하고 실제 벡터 처리는 아래쪽 드라이버에 맡긴다. 그 결과 전체 출력은 SPIRV-Tools의 1.9MB와 비교해 8.1MB가 됐다. s2h의 게더 셰이더는 입력 72KB에서 출력 1.3MB로 커졌다. 출력은 유효한 SPIR-V도 아니었다. 검증기는 368개 중 300개를 거부했는데, 각각 MatrixStride가 없는 행렬 포함 유니폼 블록 때문이었다. NIR의 기본 Vulkan 모드는 올해 새로 추가됐으며, 이를 사용해 배포되는 것은 아직 없다.

DXIL로 바꾸는 경로는 길다. spirv_to_dxil은 Mesa의 Direct3D 12 기반 Vulkan 드라이버 Dozen의 컴파일러이며, Direct3D 12에서 동작하는 WebGPU도 라이브러리로 사용한다. SPIR-V를 NIR로 바꾼 뒤 LLVM을 거치지 않고 직접 작성한 LLVM 비트코드로 변환한다. 셰이더 두 개가 실패했다. 하나는 Dozen이 지원한다고 표시하지 않은 SPV_EXT_shader_viewport_index_layer 때문이고, 다른 하나는 출력기에서 충돌이 났다.

SPIR-V에서 셰이더 소스로

아래 합계는 Naga, SPIRV-Cross, Tint가 모든 대상 형식으로 변환할 수 있었던 셰이더 358개를 기준으로 한다. Mesa는 SPIR-V를 출력하는 한 가지 경로만 측정했다.

SPIR-V에서NagaSPIRV-CrossTintMesa(최적화기 포함)
MSL38ms152ms1005ms-
HLSL33ms152ms1095ms-
GLSL31ms148ms1133ms-
WGSL30ms-787ms-
SPIR-V30ms-970ms2426ms
MSL 변환의 셰이더당 중앙값45µs87µs730µs-

Naga는 SPIRV-Cross보다 4배, Tint보다 26배 빨랐다. 2022년에는 각각 10배, 80배 차이였다. 순위는 같고 격차는 줄었지만, 데이터와 측정 기기가 다르므로 다른 도구가 빨라졌거나 Naga가 느려졌을 가능성 외에 더 말할 수 있는 것은 없다고 카라지치는 단서를 달았다.

Tint의 측정값에는 왕복 변환이 들어간다. SPIR-V에서 IR로, IR에서 WGSL AST로, AST에서 다시 IR로, 마지막으로 대상 형식으로 바꾼다. WGSL 출력은 중간 단계에서 멈추므로 Tint의 가장 빠른 경로지만, 그래도 어떤 대상에서든 SPIRV-Cross보다 5배 느렸다.

모든 SPIR-V를 입력으로 받을 수 있는 것은 아니다. SPIRV-Cross는 368개를 모두 GLSL·HLSL·MSL로 바꿨다. Tint는 9개를 거부했다. 8개는 ImageGatherExtended, 하나는 SPV_EXT_shader_viewport_index_layer를 사용했다. Naga도 같은 9개를 거부했고, 열 번째 셰이더에서는 SPIR-V 프런트엔드가 패닉을 일으켰다. 두 도구는 Vulkan의 SPIR-V 전체가 아니라 WGSL로 대응시킬 수 있는 부분집합을 읽도록 설계됐다.

WGSL에서 여러 형식으로

Tint와 Naga가 주로 비교되는 작업이다. 두 도구가 모든 대상으로 변환할 수 있었던 셰이더 365개의 합계다.

WGSL에서NagaTint
파싱·검증된 IR201ms431ms
SPIR-V221ms629ms
MSL224ms663ms
HLSL221ms741ms
GLSL218ms779ms
SPIR-V 출력의 셰이더당 중앙값84µs407µs

WGSL 입력에서 Naga는 Tint보다 약 3배 빨랐다. 2022년의 13배보다는 격차가 줄었다. 차이 대부분은 프런트엔드에서 생겼다. SPIR-V에서 읽은 WGSL을 파싱하고 검증하는 데 Naga는 201ms를 썼다. 모듈이 만들어진 뒤에는 Naga가 어떤 형식으로든 출력하는 데 약 20ms, Tint는 200~350ms가 걸렸다. Tint는 셰이더 368개를 모두 처리했다. Naga는 GLSL 출력기에서 컴퓨트 셰이더 세 개를 처리하다 패닉을 일으켰다.

실행 파일 크기

아래는 앞서 측정한 컴파일 작업만 수행하는 실행 파일의 크기다. 임베드 가능성을 따질 때 컴파일러 기능을 얼마나 골라 넣을 수 있는지도 중요하다.

구성포함 항목크기
NagaSPIR-V 독자, WGSL 출력기1.3MB
GlslangGLSL·HLSL 프런트엔드, SPIR-V 출력기2.1MB
SPIRV-CrossGLSL·HLSL·MSL 백엔드2.1MB
NagaWGSL 독자, SPIR-V 출력기2.2MB
SPIRV-Tools최적화기2.8MB
Naga전체 기능3.7MB
FXCd3dcompiler_47.dll4.7MB
TintSPIR-V 독자, WGSL 출력기5.3MB
Khronos 스택Glslang+SPIRV-Tools+SPIRV-Cross7.0MB
Tint전체 기능8.0MB
MesaSPIR-V 독자, 최적화기가 있는 NIR, SPIR-V 출력기8.6MB
MesaSPIR-V 독자, 최적화기가 있는 NIR, DXIL 출력기9.0MB
DXCdxcompiler.dll + dxil.dll19.7MB
Slangslang-compiler.dll+slang.dll+slang-glslang.dll32.3MB
Metalmetal.exe만119MB

설정 가능성은 구현마다 다르다. 독자와 출력기 하나씩만 넣은 Naga는 전체 기능을 넣은 것의 3분의 1이다. Tint는 같은 방식으로 줄여도 전체의 3분의 2이며, 같은 작업을 하는 Naga보다 4배 크다. Khronos 스택은 라이브러리 전체를 빼는 비교적 거친 방식으로 줄일 수 있다. SPIR-V에서 MSL로 바꾸는 데는 약 2MB인 SPIRV-Cross만 쓰면 되고, HLSL에서 SPIR-V로 바꾸려면 최적화기를 생략할 수 없는 Glslang과 SPIRV-Tools를 합쳐 약 5MB가 필요하다.

Naga 전체는 FXC보다 작다. 2012년의 폐쇄형 HLSL 컴파일러 하나보다 적은 공간에 백엔드 다섯 개, 프런트엔드 세 개, 검증기를 담았다.

NIR의 크기는 최적화기에서 비롯된다. 8.6MB 중 7MB는 읽기 전용 데이터이며, 대부분 Python 규칙 파일에서 nir_opt_algebraic이 생성한 패턴 매칭 표다. 독자와 출력기는 각각 수백 KB에 불과하다. Naga에 없는 최적화기는 Mesa 바이너리의 7MB를 차지한다.

결론

카라지치는 어느 컴파일러도 10가지 조건을 모두 충족하지 못한다고 결론짓는다. FXC·DXC·Metal 같은 API 컴파일러는 독점 백엔드 역할에는 적합하지만 그 밖의 조건은 거의 충족하지 못하며, 변환기보다 10~1000배 느리다. Khronos 스택은 세 프로젝트로 나뉘어 모듈식이지만 버그도 세 프로젝트에 걸쳐 있고 HLSL 프런트엔드는 폐기될 예정이다. 그럼에도 HLSL에서 SPIR-V로 바꾸는 가장 빠른 방법이다. Slang은 API와 대상 플랫폼을 갖췄지만 IR에 접근하는 API가 없다.

Naga는 가장 빠르고 작으며 설정하기 쉽고 공개 IR도 제공한다. 최적화기가 없고 Rust로 작성돼 C++ 프로젝트에서 쓰기 어렵다. NIR은 최적화기와 공개 IR이 있지만, 프로젝트에 가져다 붙일 수 있는 독립 라이브러리는 아니다.

Khronos의 2026 셰이더 생태계 조사 26~27쪽의 Q11 ‘더 높은 수준의 셰이더 IR이 프로젝트에 도움이 될까?’에서는 응답자의 거의 절반이 SPIR-V보다 높은 수준의 표준 IR이 프로젝트에 도움이 될 것이라고 답했다. 카라지치는 IR만으로는 충분하지 않고 이를 둘러싼 컴파일러가 필요하다고 주장한다. 필요한 기능은 새로 발명할 필요 없이 각 조건이 이미 누군가의 구현에 들어 있으며, Naga만으로도 10개 중 8개를 충족한다고 설명한다. 다만 아직 10개를 모두 한 라이브러리에 넣은 프로젝트는 없다고 덧붙였다.

참고 자료: 셰이더 변환 벤치마크, 2026 실시간 셰이딩 생태계 조사 보고서, NIR 옹호 글.