Zig, 새로운 @bitCast 의미론과 LLVM 백엔드 개선
1 day ago
2
- 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와 관련된 작은 수락 제안도 함께 구현됨
- 포인터 벡터와의 @bitCast 금지: #18936
- enum에 대한 @bitCast 허용: #35602의 일부
- 새 의미론은 기존 의미론과 의미 있게 다르기 때문에 표준 라이브러리, 컴파일러, 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에서는 일부 코드에서 작은 런타임 성능 향상이 생길 수 있음
-
Homepage
-
Tech blog
- Zig, 새로운 @bitCast 의미론과 LLVM 백엔드 개선