- 저수준 최적화는 컴파일러가 코드의 의도와 제약을 더 잘 이해하게 만드는 작업이며, Zig는 타입·정렬·별칭·컴파일타임 정보를 명시하기 쉬워 이 목적에 잘 맞음
- LLVM 같은 최적화 컴파일러도 항상 최선의 코드를 만들지는 않으므로, 병목 구간에서는 생성 코드 확인과 코드 조정이 여전히 필요함
- Zig는
noalias,align, 고정 배열 크기, 원소 타입을 컴파일 시점에 전달해 JavaScript 예시보다 작은 벡터화 코드를 만들 수 있음 comptime은 일반 Zig 코드를 컴파일 시점에 실행해 상수 생성, 제네릭 구현, 타입 반사, 문자열 비교 최적화 같은 메타프로그래밍을 가능하게 함- Zig의 강점은 AST를 직접 바꾸는 매크로보다 언어에 통합된 컴파일타임 실행에 있으며, 일부 런타임 값도 컴파일타임 특수화 함수로 디스패치할 수 있음
컴파일러를 믿되 확인해야 하는 이유
- 최적화는 단순히 빠른 프로그램을 만드는 기술을 넘어 비용 절감, 확장성 향상, 시스템 단순성 유지와 연결됨
- 최신 컴파일러는 LLVM 같은 백엔드로 인상적인 결과를 내지만, 일부 상황에서는 여전히 하위 최적 코드를 생성함
- 저수준 언어가 빠른 이유는 가비지 컬렉션이나 인터프리터 오버헤드가 적어서만이 아니라, 컴파일러가 이해할 수 있는 의도 정보를 더 많이 표현할 수 있기 때문임
- 컴파일러는 알고리듬이나 프로그래밍 패러다임 자체를 바꾸지 못하며, 대체로 루프처럼 제한된 범위 안에서 최적화를 수행함
JavaScript와 Zig의 배열 최댓값 예시
- JavaScript 예시는
x[i] = y[i] > x[i] ? y[i] : x[i]형태로 두 배열의 원소별 최댓값을x에 저장함 - 사람에게는 명확한 코드지만, V8이 생성한 바이트코드는 부풀어 있음
- Zig 예시는 함수 인자에 최적화에 필요한 정보를 더 구체적으로 명시함
noalias x:x가 다른 포인터와 별칭이 아님*align(64): 64바이트 정렬[65536]f64: 배열 크기와 원소 타입const: 읽기 전용 인자
- 이 정보 덕분에 컴파일러는 더 나은 코드를 만들 수 있으며, 예시에서는 벡터화된 어셈블리가 생성됨
- 동등한 Rust 코드도 거의 동일한 어셈블리를 생성함
Zig가 최적화에 유리한 지점과 한계
- Zig는 장황한 표현을 허용해 LLVM에 코드 정보를 많이 전달할 수 있음
- 최적화와 관련해 Zig가 제공하는 주요 요소는 다음과 같음
- Rust의 메모리 모델은 함수 인자가 별칭을 만들지 않는다고 컴파일러가 항상 가정할 수 있게 하지만, Zig에서는 이를 직접 지정해야 함
- 컴파일러가 Zig 함수 인자의 별칭 없음 여부를 알 수 없으면, 주석이 없는 Zig 함수는 Rust 함수보다 느릴 수 있음
- 잘 주석 처리된 LLVM IR만 기준으로 삼아도 Zig는 좋은 결과를 내지만, 더 큰 강점은 컴파일타임 실행에 있음
comptime의 역할
- Zig의
comptime은 컴파일 시점의 코드 생성을 위한 기능임 - 컴파일타임에 가능한 작업은 다음과 같음
- 상수를 생성해 바이너리에 포함
- 여러 데이터 타입에 대해 같은 hashmap 구조를 반복 작성하지 않기
- 컴파일 시점에 알려진 데이터를 바탕으로 불필요한 코드를 제거하도록 최적화 유도
- 타입을 검사·반사·생성해 제네릭 구현
comptime코드는 컴파일 시점에 실행되는 일반 Zig 코드이며, 네트워크 IO 같은 부작용은 가질 수 없음- 컴파일 시점의 에뮬레이션 머신은 컴파일 대상과 일치함
- 거의 모든 Zig 코드는
comptime으로 컴파일 시점에 실행될 수 있고, 모든 타입은 컴파일 시점에 검사·반사·생성될 수 있음
매크로와 다른 점
comptime의 목적은 매크로와 비슷하지만 동작 방식은 다름- 일부 매크로는 원시 텍스트를 바꾸고, 일부는 프로그램의 AST를 직접 수정함
- Zig의
comptime은 AST를 직접 변경하지 않으며, 토큰 붙이기 매크로와 같은 기능도 없음 - Zig는 읽기 쉬운 언어를 목표로 하므로, 관련 없는 스코프에 변수를 만들거나 수정하는 매크로 스타일과 맞지 않음
- 매크로가 할 수 있지만 Zig
comptime이 직접 하지 못하는 작업은 다음과 같음- 다른 매크로 정의
- AST 변경
- 미니 언어 또는 DSL 직접 구현
- 다만 Zig에서도 DSL을 만들 수 있으며, Zig의
print함수는comptime으로 포맷 문자열을 파싱해 데이터를 직렬화할 함수 그래프를 구성함 - 예시로 TigerBeetle account testing DSL, comath, zilliam이 있음
comptime 문자열 비교 최적화
- 일반적인 문자열 비교는 길이가 다르면
false를 반환하고, 길이가 같으면 각 바이트를 순서대로 비교함 - 이 방식은 두 문자열에서 각각 바이트를 읽어 비교해야 함
- 한쪽 문자열이 컴파일 시점에 이미 알려진 경우가 많으므로, Zig에서는 한 인자를
comptime으로 요구할 수 있음fn staticEql(comptime a: []const u8, b: []const u8) bool
"Hello!\n"같은 정적 문자열과 비교할 때 컴파일러는 길이 비교와 각 바이트의 상수 비교로 구성된 코드를 생성함- 이 섹션의 목적은 컴파일러가 자동으로 할 수 있는 최적화를 보여주는 데 그치지 않고,
comptime으로 변환을 강제해 컴파일러가 보지 못하는 기회를 열 수 있음을 보이는 데 있음
더 큰 단위 비교와 SIMD 활용
- 단순
comptime문자열 비교는 여전히 바이트 단위 비교를 수행함 - 개선된 버전은
std.simd.suggestVectorLength(u8)또는@sizeOf(usize)를 사용해 비교 블록 크기를 정함 - 문자열 길이를 먼저 확인한 뒤, 비교할 수 있는 큰 블록 수와 나머지 바이트 수를 계산함
- 각 블록은
std.meta.Int(.unsigned, block_len * 8)로 만든 정수 타입으로@bitCast해 비교함 - 남은 바이트도 별도 정수 타입으로 비교함
"Hello, World!\n"예시의 생성 어셈블리는 더 큰 레지스터를 사용하고 조건 분기 수를 줄임- 더 큰 문자열 비교에서는 더 큰 SIMD 레지스터를 사용하는 어셈블리가 생성됨
런타임 값과 컴파일타임 특수화 함께 쓰기
- Zig의
comptime은 컴파일 시점에 알려진 데이터에만 한정되지 않음 - 단순한 경우에는 여러 절차를 컴파일 시점에 생성해 두고, 런타임 값에 따라 알맞은 절차로 동적 디스패치할 수 있음
- 예시 코드는
switch (runtime_val)에서inline 0...100범위의 값은staticFn(comptime_val)로 보내고, 나머지는runtimeFn(runtime_val)로 처리함 - 바이너리 크기 증가를 원하지 않으면 완전한 런타임 구현으로 폴백할 수 있음
결론
- Zig의
comptime은 템플릿, 매크로, 제네릭, 수동 코드 생성을 대체하는 역할을 함 - 다른 언어로도 비슷한 일을 할 수 있지만, Zig에서는
comptime이 언어에 더 자연스럽게 통합되어 있음 - Zig는 실제로 유용한 상황에서 성능 좋은 코드를 작성하기 쉽게 만들며, 모든 것이 가능하지만 흥미로운 작업은 어려운 Turing tar-pit과 대비됨
- 언어 전쟁에 대해서는 튜링 완전성만으로 충분하다는 큰 관점과, 동시에 사람들이 선호하는 언어를 가질 수 있다는 입장이 함께 남아 있음
- “C가 Python보다 빠르다”처럼 언어 자체를 벤치마크 대상으로 보는 말은 잘못될 수 있으며, 실제 벤치마크 대상은 언어가 아니라 특정 코드와 구현임