Trawling

Dennis Forbes가 보고한 Zig `strspn` 병목, SQLite JSON 1.2MB 토큰화서 약 1만2000배 둔화

체스 분석 서비스 운영자 Dennis Forbes는 Linux용 SQLite의 JSON 저장 병목을 Zig의 `strspn` 구현이 남은 문자열을 반복해서 훑는 동작으로 지목했습니다. Forbes는 별도의 1.2MB 파일 토큰화 사례에서 약 1만2000배 둔화를 보고했습니다.

본문

체스 분석 서비스 운영자 Dennis Forbes는 Linux용 SQLite에서 JSON 저장이 느려진 원인을 Zig의 strspn 구현이 입력의 남은 부분을 반복해 읽는 동작으로 지목했습니다. Forbes는 이 병목을 2026년 10월 26일 블로그에 공개했습니다.

서비스는 주로 Rust로 작성됐고 SQLite를 저장소로 사용합니다. 분석 러너는 체스 게임 수백 판을 처리해 JSON 묶음을 주 서버로 보내고, 서버는 결과를 SQLite에 저장합니다. Forbes는 대량 작업을 처음 처리했을 때 약 4만 건을 넣는 데 수분이 걸렸다고 썼습니다. 이전 시험에서는 저장이 주요 병목으로 드러나지 않았지만, 이 작업에서는 CPU 코어 하나가 포화됐습니다.

성능 문제가 생긴 지점은 Rust 서비스 코드가 아니라 SQLite의 Linux 빌드 경로였습니다. SQLite는 C로 작성됐습니다. Forbes는 Mac에서 Mac ARM64와 Linux ARM64·x86-64용 바이너리를 만들 때 cargo-zigbuild와 Zig의 C 컴파일러 zig cc를 사용해 Linux용 SQLite를 빌드했습니다. 이 경로에는 Zig가 제공하는 C 라이브러리 구현이 포함됐습니다. 반면 Mac용 SQLite는 Zig로 컴파일하지 않았고, Forbes는 Mac에서는 같은 병목을 경험하지 않았다고 설명했습니다.

Zig의 libzigc는 C 표준 라이브러리 함수의 구현을 Zig 표준 라이브러리 쪽으로 옮깁니다. 이 변경은 musl 등에서 제공하던 여러 구현을 대체했습니다. strspn은 첫 번째 문자열의 앞부분에서 두 번째 인자에 포함된 문자가 연속되는 길이를 반환합니다. 두 번째 인자로 공백 문자를 주면 토큰화기가 입력 앞의 공백을 건너뛰는 데 쓸 수 있습니다.

확인한 Zig 구현은 먼저 std.mem.span으로 널 종료 문자까지 읽어 문자열 길이를 구하고, std.mem.findNone으로 해당 문자 집합에 속하지 않는 첫 위치를 찾습니다. 따라서 앞에 공백이 몇 글자뿐이어도 호출할 때마다 현재 위치부터 입력 끝까지 길이를 재야 합니다. 토큰화기가 다음 위치로 이동해 함수를 다시 부르면 남은 문자열을 또 훑습니다. 입력이 길어질수록 이 반복이 작업량을 이차적으로 키워 같은 바이트를 여러 번 읽게 됩니다.

Forbes는 1.2MB 파일을 토큰화한 별도 사례에서 약 1만2000배 둔화를 보고했습니다. 이 파일 토큰화 측정은 약 4만 건을 저장한 서비스 부하와는 별개의 관측입니다.

Forbes는 처음에 자신이 처리하는 JSON에서 공백을 미리 제거해 SQLite의 토큰화 비용을 크게 줄였습니다. 이후 다른 경로에서도 같은 문제가 생길 수 있다고 보고 Linux 빌드를 Linux 컨테이너에서 진행하도록 바꿨습니다. 이 사례에서는 애플리케이션 코드뿐 아니라 Linux용 SQLite를 빌드한 도구와 C 라이브러리 구현도 성능에 영향을 미쳤습니다.

자료: Dennis Forbes / 정리·해설: Trawling

참고 자료

오류 제보