원문 캡처 · blog.mahdibm.com
원문 캡처 · blog.mahdibm.com

Swift로 IPv4 주소를 파싱하는 새 알고리즘을 소개한다. 제작자는 M6에서 유효한 주소 하나를 처리하는 데 약 75개 명령어와 15사이클이 걸리며, 벤치마크에서는 주소 집합에 따라 평균 3.36~4.62ns를 기록했다고 밝혔다. 다만 AMD Zen 5에서는 6.12~8.77ns였고, 두 환경 모두 다른 구현이 더 빨랐다.

알고리즘과 구현

Swift에는 기본 네트워크 유형을 제공하는 통합된 플랫폼 독립 구현이 부족해, Apple의 비공개 Network 프레임워크와 오픈소스 swift-nio에 기능이 흩어져 있다고 설명한다. 제작자는 이를 개선하기 위해 swift-endpoint를 개발하고 있으며, 일부 기능은 Swift의 새 swift-networking-types에 들어갈 예정이다. 이 라이브러리는 Go의 net/netip, Rust의 std::net, Boost의 Asio API 일부와 비슷한 기능을 제공하는 것을 목표로 한다. swift-endpoint의 성능 자료에는 Darwin과 glibc의 inet API를 기준으로 한 비교도 있다.

기존 고성능 파서는 비트 연산 기법이나 벡터화, 조회 테이블 등을 활용한다. 다만 제작자는 일부 구현이 입력 뒤에 널 바이트가 있거나, 입력이 패딩되어 있거나, 최소 16바이트가 제공된다는 계약을 전제로 해 Swift API에는 어울리지 않을 수 있다고 지적한다. 조회 테이블은 캐시에 필요한 데이터가 이미 들어 있다는 벤치마크 가정을 둘 수 있고 메모리 사용량과 지연에도 영향을 준다. SIMD 명령어는 CPU 세대마다 지원이 달라 이식성이 낮아질 수 있다. 기존 연구와 구현에 기여한 다니엘 르미르(Daniel Lemire)와 보이치에흐 무와(Wojciech Muła) 등 여러 사람의 선행 작업이 새 알고리즘의 바탕이 됐다고 밝혔다. Swift Foundation의 Base64 인코딩을 자동 벡터화로 약 3배 빠르게 한 PR도 사례로 들었다.

핵심은 서로 독립적인 계산을 동시에 수행하도록 입력의 여러 구간을 한꺼번에 읽는 것과, 한 레지스터에서 여러 값을 다루는 SWAR 기법이다. 64비트 레지스터에 8개의 8비트 값을 담아 한 번에 연산하면 각 값을 따로 처리하는 횟수를 줄일 수 있다. 유효한 IPv4 문자열은 1.1.1.1처럼 7바이트 이상 15바이트 이하이며, 각 구간의 위치는 정확히 고정돼 있지 않아도 범위를 알 수 있다. 첫 번째 구간과 뒤따르는 점은 앞에서 네 문자 안에, 마지막 구간은 뒤에서 네 문자 안에 놓인다. 가운데 구간도 앞이나 뒤에서 읽었을 때 정해진 범위에 있다.

숫자 문자의 ASCII 값은 0x30~0x39이므로 각 바이트를 0x30과 XOR하면 숫자 값이 0x00~0x09로 바뀐다. 이때 점은 0x1E가 되어 숫자와 구별된다. 구현은 주소 문자열에서 32비트 단위로 여러 바이트를 읽고, 겹치는 창(window)을 만들어 구간과 점의 위치를 찾는다. 처음에는 네 창을 처리했지만 IPv4에는 점이 세 개뿐이므로 세 창에서 점을 찾은 뒤, 두 번째와 세 번째 점 사이에 있는 구간만 좁은 범위에서 다시 읽도록 바꿨다. 이후 창을 고르는 방식을 더 조정해 M6에서 유효한 IPv4 문자열 하나당 약 75개 명령어, 15사이클로 줄였다고 설명한다.

제작자는 최종 구현이 swift-endpoint의 테스트 모음과 자신이 실행한 퍼즈 테스트를 통과했다고 밝혔다. 최종 구현 코드에는 동작을 설명하는 주석도 포함돼 있다.

벤치마크 조건과 결과

shapes는 무작위로 만든 유효 IPv4 주소 400만 개를 사용했다. 제작자는 이 규모가 M6와 AMD 프로세서의 분기 예측기가 결과를 왜곡하지 않도록 하기에 충분하다고 설명한다. real32는 널리 쓰이는 IP 주소 32개를 대상으로 했으며, 분기 예측을 어렵게 하려고 George Marsaglia의 xorshift64 13/7/17 변형과 상수 시드를 이용해 순서를 섞었다. 난수 생성 오버헤드는 작지만 측정값에서 빼지 않았다. 주소 집합에 따라 성능은 달라질 수 있으나 상위 5개 순위는 바뀌지 않았다고 한다.

테스트 환경은 Apple M6·macOS 27에서 Swift 6.4, Apple clang 21.0.0, libc++ 22.1.6, Rust 1.98.1, Go 1.27.2를 사용했다. AMD Zen 5·Ubuntu 24.04 환경은 Swift 6.4, clang 21.0.0, glibc 2.39, libstdc++ 13.3, Rust 1.98.1, Zig 0.17.0, Go 1.27.2로 구성했다. M6에서 SSE를 사용하는 구현은 sse2neon으로 실행했으며 표에서 → NEON으로 표시했다. 비율은 swift-endpoint를 기준으로 한다.

M6의 shapes에서 swift-endpoint는 3.36ns로 2위, real32에서는 4.62ns로 2위였다. 1위인 simdzone sse_inet_aton은 각각 2.84ns와 3.61ns였다. AMD Zen 5에서는 swift-endpoint가 shapes 6.12ns, real32 8.77ns로 모두 3위였다. 두 테스트의 1위는 simdzone sse_inet_aton으로 각각 3.74ns, 4.70ns였다. AMD에서는 Cesarano BMI2(Zig)가 각각 5.69ns, 8.01ns로 2위를 기록했다.

M6에서 swift-endpoint보다 느린 비교 대상에는 Muła의 여러 버전, fast_float v8.3.2, ada, Rust 표준 라이브러리, Go net/netip, std::from_chars, Apple inet_pton 등이 포함됐다. AMD에서는 Muła 구현들, fast_float, ada, Rust 표준 라이브러리, glibc inet_pton, Go net/netip 등이 비교됐다. 반면 두 CPU 환경 모두 simdzone 구현이 swift-endpoint보다 빨랐다. 제작자는 르미르의 sse_inet_aton이 매우 빠르지만 조회 테이블을 많이 사용하고 입력 폭을 계약상 요구한다고 덧붙였다.

활용과 해석

이 결과는 IPv4 문자열을 빠르게 파싱하는 구현을 Swift 네트워크 라이브러리에 적용하려는 사람에게 참고가 된다. 성능 수치는 특정 프로세서와 운영체제, 컴파일러 도구 모음, 입력 집합에 따른 벤치마크 결과다. 특히 제작자가 제시한 M6의 약 3ns 수치는 다른 결과와 구분해야 한다. 실제 표에서 swift-endpoint는 M6의 shapes에서 3.36ns, real32에서 4.62ns였으며, AMD Zen 5에서도 1위는 아니었다.

이 글은 블로그의 첫 게시물이라고 밝히며, 벤치마크 코드의 정리된 버전을 이후 공개하고, 더 최신 x86_64 프로세서에서 테스트하고, AWS 계정의 임시 EC2 인스턴스에서 벤치마크를 쉽게 실행하는 방법을 마련할 계획이라고 적었다.