Zig, 새로운 @bitCast 의미론과 LLVM 백엔드 개선
(ziglang.org)- Zig master 브랜치에 LLVM 백엔드의 비 ABI 정수 처리 개선과 새
@bitCast의미론이 병합되어, 최적화 문제와 언어 동작 불일치를 함께 정리함 u4,i13,u40같은 임의 비트폭 정수는 SSA 값에서는 bit-int로 다루되, 메모리 저장 시 ABI 크기 정수로 확장하는 방식으로 바뀜- 기존
@bitCast는 메모리 바이트 재해석에 가까웠지만, 새 정의는 타입의 논리적 비트 배열을 기준으로 해석해 endian 의존성을 줄임 - 변경은 LLVM·C 백엔드와
comptime실행까지 확장됐고, 표준 라이브러리·컴파일러·compiler_rt의 관련 사용처도 함께 점검됨 - 놓치던 LLVM 최적화가 되살아나면서 Zig 컴파일러 자체에서 약 5% 성능 개선이 관찰됐고, 0.17.0에서 일부 런타임 성능 향상을 기대할 수 있음
LLVM 백엔드의 임의 비트폭 정수 처리 변경
- Zig는 기존에
u4,i13,u40같은 임의 비트폭 정수 타입을 LLVM IR의 bit-int 타입인i4,i13,i40으로 직접 lowering해왔음 - 이 방식은 LLVM의 메모리 표현 의미론이 최적화기에 불필요한 제약을 만들었고, Clang이 이런 LLVM IR을 만들지 않아 LLVM 내부 경로도 충분히 테스트되지 않았음
- 지난 몇 년 동안 실제로 최적화 누락과 miscompilation 사례가 관찰됨
- 새 방식은 SSA 값 조작에는 bit-int 타입을 유지하되, 메모리에 저장할 때
i8,i16,i32같은 ABI 크기 타입으로 zero-extend 또는 sign-extend함 - 이 lowering은 C의
_BitInt(N)를 Clang이 lowering하는 방식과 맞아, LLVM에서 더 잘 지원되는 경로로 기대됨
기존 @bitCast의 한계
- 기존
@bitCast는 개념적으로 다음 동작에 가까웠음- 피연산자 값의 포인터를 얻음
- 그 포인터를 목적지 타입 포인터로 캐스팅함
- 해당 포인터에서 값을 로드함
- 즉 기존 정의는 타입의 논리 구조보다 메모리의 바이트 재해석에 가까웠음
- 시간이 지나며 실제 동작은 이 정의에서 벗어났고, 대부분의 타깃에서
@sizeOf(u24)가@sizeOf([3]u8)보다 큰데도[3]u8을u24로@bitCast하는 것이 허용됨 - LLVM 백엔드는 충분히 명세화되지 않은
@bitCast의미론을 구현하고 있었고, 정수 타입의 메모리 저장 방식을 바꾸자 컴파일러 테스트 스위트에서 Illegal Behavior와 크래시가 발생함 - LLVM 백엔드에 기존 동작을 흉내 내는 로직을 추가하는 대신, 새로운
@bitCast정의를 전반적으로 구현하는 방향이 선택됨
새 @bitCast 의미론
- 새 의미론은 2024년에 제출되어 수락된 언어 제안 #19755를 기반으로 함
- 이 의미론은 이미 self-hosted x86_64 백엔드에 구현되어 있었고, 이번 변경으로 LLVM·C 백엔드와
comptime실행까지 확장됨 - 새
@bitCast는 메모리 바이트가 아니라 타입을 논리적으로 표현하는 비트 순서를 기준으로 동작함u5는 least-significant bit부터 most-significant bit까지 5개의 논리 비트로 구성됨[2]u5는 첫 번째 원소의 5비트 뒤에 두 번째 원소의 5비트가 이어진 10개의 논리 비트로 구성됨
u8을 같은 크기의i8로 바꾸는 경우처럼 단순한 정수 간 변환은 비트가 그대로 유지되고, 최상위 비트가 부호 비트로 해석됨- 정수 타입과
packed struct또는packed union사이의@bitCast의미론도 유지됨
배열·벡터에서 달라지는 동작
- 새 의미론이 기존과 달라지는 지점은 배열과 벡터 같은 aggregate 타입이 관련될 때임
- 예를 들어
[2]u8을u16으로@bitCast하면 기존 의미론에서는 대상 endian에 따라 결과가 달랐음- big-endian 타깃에서는 첫 번째 배열 원소가 상위 8비트가 됨
- little-endian 타깃에서는 첫 번째 배열 원소가 하위 8비트가 됨
- 새 의미론은 논리적 비트 표현만 고려하므로 endian에 독립적이며, 모든 타깃에서 첫 번째 배열 원소가 하위 8비트가 됨
- 일반적으로는 little-endian 타깃에서의 기존 동작과 더 가까움
[2]u3을@Vector(3, u2)로 변환하는 것처럼 비정형적인 변환도 가능함- 배열의 논리 비트를 이어 붙인 뒤 2비트 단위로 읽어 벡터 원소를 구성함
- 정수를
@Vector(n, u1)로@bitCast해 개별 비트 벡터로 분해하는 용도에도 쓸 수 있음
함께 반영된 제안과 마이그레이션
- 이번 작업 중
@bitCast와 관련된 작은 수락 제안도 함께 구현됨 - 새 의미론은 기존 의미론과 의미 있게 다르기 때문에 표준 라이브러리, 컴파일러,
compiler_rt같은 지원 라이브러리의@bitCast사용이 점검됨 - 관련 PR은 codeberg.org/ziglang/zig/pulls/35711이며, master에 병합되면서 여러 이슈도 함께 닫힘
- 변경된 의미론과 권장 마이그레이션 절차는 Zig 0.17.0 릴리스 노트에 정리될 예정임
0.17.0에서 기대되는 성능 효과
- 원래 목표였던 LLVM 백엔드의 비 ABI 정수 lowering 변경은 놓치던 최적화를 되살리는 데 성공함
- 관련 결과는 demonstrably successful로 확인할 수 있음
- Zig 컴파일러 자체는 내부적으로 임의 비트폭 정수를 많이 쓰지 않는데도, 더 나은 최적화 덕분에 약 5% 성능 개선을 보임
- 0.17.0에서는 일부 코드에서 작은 런타임 성능 향상이 생길 수 있음
댓글과 토론
Lobste.rs 의견들
-
글에서 말하는 논리적 비트 표현이 엔디언 독립적이라고 하지만, 실제 설명은 빅엔디언 비트 순서나 바이트 순서를 지원하지 않는 명백한 리틀엔디언 방식으로 보임
- 여기서 엔디언 독립적이라는 뜻은 리틀엔디언/빅엔디언 아키텍처 사이에서 동작이 달라지지 않는다는 의미로 보임
-
2026년 6월 25일자 새 개발 로그로, 새
@bitCast의미론과 LLVM 백엔드 개선이 최근 풀 리퀘스트에 병합됐다는 내용임 -
흥미롭지만, 드물게 테스트되는 빅엔디언 대상에서 아래처럼 작성된 코드가 갑자기 깨질 수 있지 않을까 싶음
비-Zig 의사코드로 쓰면:if target_is_little_endian { my_int = @bitCast(my_array); } else { my_int = @bitCast([my_array[1], my_array[0]]); }- 나도 그 생각을 했지만, 결국 피할 수 없는 변경을 미루면 문제가 더 커질 뿐이라고 봄
실제로 큰 문제는 아닐 듯한데, Zig 저장소의 수천 개@bitCast중 이 변경의 영향을 받은 건 100개보다 훨씬 적었던 것 같음
솔직히 배열/벡터와 스칼라 사이 변환에서@bitCast가 어떻게 동작하는지 대부분의 Zig 사용자가 정확히 알고 있었다고도 생각하지 않음. 기존에는 작성자 시스템에서만 테스트되어 리틀엔디언에서만 동작하던 코드가 이제는 어디서나 동작하게 되는 경우도 많을 듯함
- 나도 그 생각을 했지만, 결국 피할 수 없는 변경을 미루면 문제가 더 커질 뿐이라고 봄
-
예전 C 프로그래머로서, C의 비트 필드는 아키텍처마다 동작이 이식 가능하지 않아 별로 인기가 없었던 걸로 기억함
새 Zig@bitCast의미론은 서로 다른 아키텍처에서도 같은 결과를 주는 이식 가능한 추상 의미론이라서 딱 필요했던 방향이라고 봄
최근 내 언어의 비트 필드와 비트 캐스트 설계를 하고 있어서, 내 코드가 어떻게 동작해야 할지 명확히 하려고 Zig 설계와 구현 문서를 더 자세히 볼 생각임- C 비트 필드에 대한 Zig의 주된 대안은 아마
packed struct와packed union이고, 둘 다 새@bitCast정의와 잘 맞도록 정의되어 있음
packed struct는 필드의 비트를 “기반 정수”에 채워 넣는 방식임. 예를 들어 필드가bool,u6,i9이고 기반 정수가u16이면,u16의 최하위 비트가bool, 다음 6비트가u6, 나머지 9비트가i9가 됨. 즉 Zig의 packed struct는 여러 시프트와 마스크 위에 얹힌 문법 설탕에 가까움
packed union도 기반 정수를 가지지만, 모든 필드가 기반 정수와 정확히 같은 비트 수를 써야 함. 그래서 한 필드에 저장하고 다른 필드에서 읽는 동작은 새 의미론의@bitCast와 거의 동일함. 다만packed union/packed struct필드는 배열이나 벡터 타입을 가질 수 없음
개인적으로는 이런 도구들이 “비트 관련 구조”를 표현하기에 잘 맞는다고 봄. 여러 값을packed struct로 비트 패킹해 C 비트 필드처럼 쓸 수 있고, 비트 연산 위의 문법 설탕이라 C에서 타입 안전하지 않은 매크로 더미로 처리하던 비트 플래그도 깔끔하게 표현 가능함
예를 들어 RWX 접근 플래그는 C에서는ACCESS_READ,ACCESS_WRITE,ACCESS_EXEC매크로와uint8_tAPI로 받을 수 있지만, Zig에서는Access = packed struct(u8)로read,write,exec,reserved필드를 정의하고 API에서Access를 받을 수 있음
packed struct와packed union을 쓰면 꽤 이상한 비트 배치도 표현할 수 있음. Mach-O 객체 포맷의 심볼 테이블 엔트리에는 역사적 이유로 보이는 특이한n_type필드가 있는데, 이를packed union(u8)안에bits: packed struct(u8)와stab: enum(u8)형태로 모델링할 수 있음
이n_type값을 다룰 때 수동 시프트나 마스킹이 필요 없음.n_type.bits.is_stab != 0을 확인하고 참이면n_type.stab으로switch하면 되고, 아니면n_type.bits의 다른 필드를 보면 됨. 반대로.{ .stab = .gsym }나.{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } }처럼 값을 만들 수도 있음
원 글 주제와는 다른 언어 기능으로 조금 길어졌지만, 새 언어 설계에 참고할 만한 걸 찾는다면 Zig의packed struct와packed union을 직접 써보면 좋겠음. 단순하지만 꽤 괜찮은 도구라고 봄
- C 비트 필드에 대한 Zig의 주된 대안은 아마