메모리 누수 리포트

메모리 누수 리포트

코드에서 Memory를 할당할 때 Zig Standard Library의 테스트용 할당자인 std.testing.allocator를 쓰면, 기본 테스트 러너가 이 테스트 할당자로 찾아낸 누수를 알아서 리포트해 줘요.

const std = @import("std");

test "detect leak" {
    const gpa = std.testing.allocator;
    var list: std.ArrayList(u21) = .empty;
    // missing `defer list.deinit(gpa);`
    try list.append(gpa, '☔');

    try std.testing.expectEqual(1, list.items.len);
}

출처: Zig Documentation

본문

이 예시를 그대로 테스트로 돌려볼게요. testing_detect_leak.zig라는 파일에 위 코드를 저장한 뒤 zig test로 실행하면, 테스트 자체는 통과해요(1/1 OK). 그런데 실제 결과를 잘 보면 SafeAllocator가 로그를 남기면서 어떤 주소가 할당되고도 해제되지 않았다고 알려줘요.

$ zig test testing_detect_leak.zig
1/1 testing_detect_leak.test.detect leak...OK
[SafeAllocator] (err): leaked [addr: 7f925e738010, len: 132 (0x84) align: 4] allocated at:
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/array_list.zig:1372:56: 0x1254ab1 in ensureTotalCapacityPrecise (std.zig)
                const new_memory = try gpa.alignedAlloc(T, alignment, new_capacity);
                                                       ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/array_list.zig:1346:51: 0x12546f9 in ensureTotalCapacity (std.zig)
            return self.ensureTotalCapacityPrecise(gpa, growCapacity(new_capacity));
                                                  ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/array_list.zig:1403:41: 0x1253cdd in addOne (std.zig)
            try self.ensureTotalCapacity(gpa, newlen);
                                        ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/array_list.zig:1024:49: 0x1253b77 in append (std.zig)
            const new_item_ptr = try self.addOne(gpa);
                                                ^
/home/ci/work/zig-bootstrap/zig/doc/langref/testing_detect_leak.zig:7:20: 0x12539d8 in test.detect leak (testing_detect_leak.zig)
    try list.append(gpa, '☔');
                   ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/compiler/test_runner.zig:295:25: 0x1207381 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/compiler/test_runner.zig:74:28: 0x1206b52 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
(additional stack frames may have been skipped...)

All 1 tests passed.
1 errors were logged.
1 tests leaked memory.
error: the following test command failed with exit code 1:
/home/ci/work/zig-bootstrap/out/zig-local-cache/o/1ca6c6783e7e0eafd62c4b04d5deaebb/test --seed=0xe17d7a49

여기서 핵심은 어디에 있을까요. 제일 아래쪽 error: 한 줄 때문에 테스트 명령이 exit code 1로 실패했다는 점이에요. 테스트 자체는 모두 통과했는데도, 누수가 잡혔으니(1 tests leaked memory) 전체 실행은 실패로 넘어가요. 그리고 무엇보다 additional stack frames may have been skipped... 위쪽의 스택 트레이스가 어디서 메모리가 할당됐는지 정확히 짚어 주죠. 이 예시에서는 list.append(gpa, '☔')가 호출되면서 ArrayList가 내부 버퍼를 확장한 지점(ensureTotalCapacityPrecise)이 누수 지점으로 찍혀요.

왜 누수가 생겼을까요. 코드 주석에 이미 힌트가 들어 있지만, defer list.deinit(gpa);가 빠져 있어서 테스트가 끝날 때 리스트가 할당한 메모리를 해제하지 않았죠. 리스트를 만들 때 잡아둔 메모리를 defer로 정리해 주면 이 리포트는 사라져요.

이처럼 테스트용 할당자(std.testing.allocator)를 쓰면 무심코 흘려보내기 쉬운 누수도 눈에 띄게 잡아내요. 테스트가 "통과"했다고 끝이 아니라, 터미널 출력에 leakederrors were logged가 없는지까지 확인하는 습관이 필요해요. 메모리는 수동으로 관리하는 언어라서, 컴파일러가 스스로 잡아주지 않는 이런 부분을 테스트가 대신 지켜주는 거죠.

더 알아보기