Shader Compiler Survey
Over the years of maintaining shaderc I formed an opinion of how a shader compiler should look. Not the language, I don’t care much about the language (as long as it is C‑like 😉), but the thing itself: what goes …
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
Over the years of maintaining shaderc I formed an opinion of how a shader compiler should look. Not the language, I don’t care much about the language (as long as it is C‑like 😉), but the thing itself: what goes in, what comes out, and how much of it I am allowed to touch. I made a list of what I think an ideal shader compiler looks like, shaped by offline and embedded use: platforms that do not allow compiling shaders at runtime, shipping optimized IR, keeping the executable small when a shader compiler is used at runtime, and being able to inject or skip stages. Then I went through what exists today, checked each one against the list, and benchmarked them all.
-
Text in, text out. The compiler must take text, lex and parse it, run some optimizations (or not), and be able to print text again. This is the glsl‑optimizer case: the driver is bad at optimizing, the API accepts only source, and I want at least basic passes done before the shader ships.
-
Any proprietary compiler can be attached at the end. On NDA (a.k.a. video game console) platforms compiling or generating code at runtime is not an option, and the platform compiler might sign the binary with an algorithm nobody outside knows. The last stage has to be replaceable with a black box.
-
Any part of it can be skipped. Including all of it. When I need a feature the compiler doesn’t support, and there is no time to add it properly, I want to go straight through to the back‑end compiler instead of bolting something on the side.
-
Things can be injected at any stage. I must be able to get at the IR between stages and transform it into what I need.
-
It is configurable. Everything I don’t need for the target I’m building for should be strippable, and the binary should get smaller when I strip it.
-
It is a library. Embeddable, and not structured as a command line tool that happens to have a main() you can call. Can go from text to shader binary. Or could be limited, where we skip lexing, parsing and optimization and just convert IR to target shader binary. “Embeddable” in this context also means that shader compiler should not dominate binary size of executable if it’s embedded. If we have 100MiB library that gets linked with 1MiB executable, that is not really “embeddable”.
-
The IR is serializable. So that optimized shader IR can be shipped, and translated/emitted for the specific backend at runtime. This doesn’t work everywhere, but where it does it’s a nice feature.
-
The IR can be assembled at runtime, without the compiler. If generating a shader is simple, and there is nothing to gain from optimization passes, I should be able to emit IR directly and link only the back‑end. This is a binary size optimization.
-
It runs where I build. It must produce shaders the API can ingest from any major desktop OS: Windows, Linux, and macOS.
-
It should be fast. Even if fast means reentrant and have to use multiple threads to achieve high speed when compiling multiple shaders. Compiling multiple shaders is not dependent on each other, so a shader compiler should not force sequential compiling.
Nothing on this list is exotic. None of it is impossible or unheard of; it is how LLVM describes itself:
The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
How they stack up against the list. The last row counts a plain yes as a point and anything qualified as partly:
FXC is one function, D3DCompile: HLSL in, DXBC out, nothing in between for you. Closed, Windows only, frozen at Shader Model 5.1. Close to zero on the list, and exactly what ideal 2 is for.
DXC is LLVM 3.7 with an HLSL front‑end, so everything on my list exists inside it and almost none of it is reachable: IDxcOptimizer runs passes by name, and that is it. One 18MB DLL, whether you want the SPIR‑V back‑end or not. That back‑end matters, though. With Glslang’s HLSL front‑end on its way out, DXC is where HLSL for Vulkan is headed.
Metal compiler is Clang. metal compiles MSL to AIR, which is LLVM bitcode, as a command line tool and nothing else; the only in‑process Metal compiler is the one inside the OS. Apple ships it for macOS and Windows, not Linux.
Glslang+SPIRV‑Tools+SPIRV‑Cross does ideal 1 in full, and is the best at 3 by accident: three libraries by three sets of authors with a binary format between them, so every seam is a place to stop or to insert your own. The price is the same fact from the other side: nobody owns the whole, and a front‑end can be deprecated without the back‑end noticing. For HLSL the optimizer is not optional; what Glslang emits is not legal SPIR‑V until SPIRV‑Tools has legalized it. More on that in the benchmark.
Tint has a proper IR, a builder, transforms, and compile‑time switches for every reader and writer, which on paper covers ideals 4, 5 and 8. In practice it is internal C++ with no stability promise, built by Dawn, and the public header tint.h offers Initialize, Shutdown and SpirvToWgsl. No optimizer, by design. Its SPIR‑V reader does not feed the writers directly but round‑trips through a WGSL AST; remember this for the benchmark.
Naga is the closest to the list. naga::Module is plain public data: build it by hand, walk it, rewrite it, hand it to any back‑end. Every front‑end and back‑end can be compiled out, the module serializes, and front‑end, validator and back‑ends are separate calls. What it lacks is an optimizer and a C API. What comes out is legal and unoptimized; if you want it optimized, something else has to do it. It is written in Rust, and from C++ that means writing a C wrapper around its types yourself, and fixing it every time Naga changes them, which is often. For a Rust project it is the answer. For me it is ideal 6 with a giant asterisk: for a C++ project it is close to non‑viable.
NIR is the IR inside every Mesa driver and Microsoft’s OpenCL and OpenGL layers on Direct3D 12, which makes it the most used shader compiler there is, and it has most of what my list asks for, built for a driver’s needs rather than mine. Passes are C functions called in any order, nir_builder builds IR by hand, nir_serialize writes it out, and there is a real optimizer. Faith Ekstrand’s In defense of NIR explains why it is built this way. SPIR‑V and GLSL come in; SPIR‑V, DXIL and MSL come out, next to the hardware back‑ends. None of it is a library: no public header, no stable API, no release, and the serialized form is for the build that wrote it. Each writer was written for one driver; the MSL one would not run without the driver’s descriptor lowering, so it is missing from the benchmark.
Slang is the only one designed as a whole compiler for more than one target: its own front‑end, IR, passes and writers, a proper API, every platform, and modules stored as serialized IR. It takes ideal 2 most seriously: FXC, DXC and the rest are “downstream compilers” it knows how to call, with a pass‑through mode that hands them the source untouched. What it lacks is the middle of my list: the IR is internal, with no API to read, transform or build it. And it comes as one piece: slang-compiler.dll is 26MB, plus a 6MB Glslang and SPIRV‑Tools for SPIR‑V.
There is a shader translation benchmark from 2022, by Naga’s author, in‑process and with I/O excluded, in which Naga won every round by an order of magnitude. It is four years old; these are today’s numbers.
Corpus. Every vertex, fragment and compute shader from the bgfx examples: 368 shaders, 120 vertex, 203 fragment and 45 compute, from a four‑line vertex shader to FSR and ASSAO. The same shaders exist in every language the compilers take:
Method. One C++ executable links Glslang, SPIRV‑Tools, SPIRV‑Cross and Tint statically, and loads d3dcompiler_47.dll and dxcompiler.dll. Naga gets its own Rust executable, and Slang is used through the DLLs from its release package. Shaders are loaded into memory first, every compile runs 10 times (5 for Slang), the fastest run counts, and the result is the sum over the corpus. Single thread. Metal has no library, so it is one process per shader, best of 3, with process startup measured separately.
Versions. Glslang 16.1.0, SPIRV‑Tools v2025.5 and Tint as pinned by Dawn 4b91cc492; SPIRV‑Cross from August 2026; Naga 30.0.1; Slang v2026.19; DXC 1.10.2605; FXC 10.0.26100; Metal 32023.850; Mesa e2ab668. FXC and DXC at -O3, SPIRV‑Tools with its legalization and performance passes, Slang at its defaults, Tint driven the way its command line tool does it, Mesa through spirv_to_nir, then the passes and SPIR‑V writer of Zink, its OpenGL‑on‑Vulkan driver, or spirv_to_dxil. MSVC release builds, Ryzen 9 7950X, Windows 10.
Every number here is all 368 shaders of the corpus, each compiled on its own to what its API’s driver loads, best of ten runs, summed.
Naga and Tint are in a different race. Neither reads HLSL, so they start from WGSL, and neither optimizes; they translate. What comes out needs no SPIRV‑Tools either: all 368 of Tint’s modules pass the validator as emitted, 366 of Naga’s do, the other two failing on a layout decoration on a workgroup array. Driver‑ready SPIR‑V comes out of Naga in 0.22s for the whole corpus and out of Tint in 0.64s. The rest of the table starts at 5s.
Glslang is fast, SPIRV‑Tools is not, and Glslang on its own is not enough. Parsing 316K lines of HLSL and emitting SPIR‑V takes Glslang 0.93s, 2.1ms per shader. But Glslang’s own help warns that -Od disables optimization; may cause illegal SPIR-V for HLSL, and it does: 173 of the 368 modules fail the validator until SPIRV‑Tools has legalized them, and none after. So the two are a pair, and the pair takes 4.96s. Legalizing and optimizing is 81% of that. HLSL to MSL through the whole stack, Glslang, SPIRV‑Tools, SPIRV‑Cross, takes 5.1s, and the optimizer is 79% of it.
FXC is two compilers. Its median is 4.8ms, less than half of DXC’s. Its total is 2.5 times DXC’s. Three shaders from one example take 4.1s, 2.9s and 1.6s, more than the other 365 put together. The same example has the slowest shaders for DXC, SPIRV‑Tools, SPIRV‑Cross and Tint too, but there the worst case stays around a second. This is the FXC everybody who shipped a D3D11 game remembers.
DXC is LLVM, and it shows. Its floor is 11ms, for a shader Glslang and SPIRV‑Tools together finish in 3ms. And DXC to SPIR‑V is the same job as Glslang+SPIRV‑Tools, HLSL in, optimized SPIR‑V out: DXC takes 8.6s, Glslang and the optimizer 5.0s. DXC also failed two compute shaders that it compiles to DXIL without complaint, and that Glslang compiles to SPIR‑V.
Slang is the slowest way from HLSL to SPIR‑V. Three times the Khronos stack, 1.7 times DXC. Almost none of it depends on the target: printing HLSL back out costs the same 33ms per shader as emitting SPIR‑V. The time is in the front‑end. A one‑line vertex shader goes through in 0.5ms, so the 30ms is the thousand lines of helper functions in front of every shader, which Slang checks whether the shader calls them or not. On the other hand it has no bad day: its slowest shader is 183ms, where FXC, DXC and SPIRV‑Tools each have one around a second or more.
Slang is also stricter than the HLSL compilers. None of the 368 shaders compiled as they were, so the HLSL was edited until they did. FXC, DXC and Glslang accept all of this, Slang does not:
With those five edits to the shaders, Slang compiles all 368 to SPIR‑V, GLSL and HLSL, 366 to MSL, and 360 to WGSL. What is left are things the target doesn’t have, like InterlockedAdd in WGSL.
Metal is a process. Starting metal and doing nothing costs 10ms. Take that out and it is still 47ms per shader, 18.7s for the corpus, on the easiest input of the lot: 37K lines of machine‑written MSL against the 316K lines of HLSL the others chew through.
NIR and SPIRV‑Tools do the same job here: take the SPIR‑V Glslang emi
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。