inline while

inline while

while 루프도 inline으로 만들 수 있어요. 그러면 루프가 언롤(unroll) 되는데, 그 덕분에 컴파일 타임에만 동작하는 일들을 코드 안에서 처리할 수 있게 됩니다. 대표적인 게 타입을 일급 값(first class value)처럼 다루는 거예요. 어떤 뜻인지 바로 코드로 확인해 볼게요.

출처: Zig Documentation

본문

while 루프 앞에 inline을 붙이면 그 루프는 컴파일 타임에 펼쳐집니다(unrolled). 루프가 풀리면서 반복마다 본문이 그대로 복제되는 셈이라, 컴파일 타임에만 가능한 일들—예를 들어 타입을 값처럼 다루는 코드—이 그 안에서 동작할 수 있어요.

const expectEqual = @import("std").testing.expectEqual;

test "inline while loop" {
    comptime var i = 0;
    var sum: usize = 0;
    inline while (i < 3) : (i += 1) {
        const T = switch (i) {
            0 => f32,
            1 => i8,
            2 => bool,
            else => unreachable,
        };
        sum += typeNameLength(T);
    }
    try expectEqual(9, sum);
}

fn typeNameLength(comptime T: type) usize {
    return @typeName(T).len;
}
$ zig test test_inline_while.zig
1/1 test_inline_while.test.inline while loop...OK
All 1 tests passed.

여기서 inline while이 하는 일이 보이죠? 루프 변수 icomptime var라서, 반복할 때마다 switch (i)컴파일 타임에 결정됩니다. 그 덕분에 각 반복마다 Tf32, i8, bool로 고정되면서 typeNameLength(T)에 타입이 그대로 전달돼요. Ttype형 값이라서, 만약 런타임 변수였다면 @typeName(T).len 같은 호출은 아예 성립하지 않아요. 합계가 3 + 2 + 4 = 9가 되는 것도 전부 이렇게 타입이 컴파일 타임에 확정되기 때문에 가능한 거예요.

그런데 이렇게 반복문을 강제로 펼치는 건 그냥 해서 좋을 게 별로 없어요. 그래서 inline 루프를 쓸 만한 이유는 딱 두 가지로 좁혀서 권장합니다.

  • 루프가 컴파일 타임에 실행되어야 의미가 맞는 경우. 위 예시가 정확히 이 경우예요.
  • 이렇게 강제로 펼치는 게 측정 가능할 만큼 더 빠르다는 걸 벤치마크로 증명한 경우.

즉 "펼치면 더 빨라지지 않을까" 하는 막연한 기대만으로는 쓰지 않는 게 좋아요. 컴파일 타임 의미론이 필요할 때, 혹은 벤치마크로 이득이 확인됐을 때만 쓰는 게 맞아요.

더 알아보기(Learn more)

  • if — 컴파일 타임 분기 처리와 관련돼요.
  • Optionals — 값이 없을 수 있는 타입을 다루는 방법이에요.
  • Errors — 컴파일 타임에 검증되는 오류 처리 흐름이에요.
  • comptime — 컴파일 타임 평가가 어디까지 전파되는지 다뤄요.
  • unreachable — 도달할 수 없는 분기를 컴파일러에 알려주는 방법이에요.