Lobsters

Hacking the Go compiler to efficiently map IPv4 to IPv6

Go 컴파일러를 손봐 IPv4를 IPv6로 효율적으로 매핑하기

Go의 netip.Addr에는 IPv4 주소를 IPv4-mapped IPv6 주소로 바꾸는 To6() 메서드가 없습니다. 글쓴이는 권장되는 AddrFrom16(ip.As16()) 방식이 네이티브 구현보다 약 8배 느린 이유를 분석하고, 컴파일러 최적화로 성능 차이를 없애는 방법과 그 과정의 부작용을 설명합니다.

AI 요약

Go의 netip.Addr는 IPv4 주소를 IPv4-mapped IPv6 형식으로 저장하지만, IPv4로 되돌리는 Unmap()과 달리 IPv6 형식으로 바꾸는 To6() 메서드는 제공하지 않습니다. Go 관리자는 netip.AddrFrom16(ip.As16())를 쓰고 컴파일러가 최적화하도록 권합니다. 하지만 글쓴이가 측정한 Go 1.27.1 환경에서는 이 방식이 네이티브 메서드보다 약 8배 느렸습니다. 글은 이 변환을 빠르게 만들 수 있는 세 가지 구현 방식과 컴파일러 최적화 실험을 다룹니다.

구현 선택지와 성능

표준 라이브러리 안에서는 간단히 Addr의 주소 데이터는 그대로 두고 주소 계열을 나타내는 z 필드만 IPv6용 값으로 바꾸면 됩니다. 패키지 바깥에서는 z 필드에 접근할 수 없으므로 AddrFrom16(ip.As16())를 감싸는 헬퍼를 쓰거나, unsafe로 구조체 메모리를 직접 바꾸는 방법이 있습니다. AMD Ryzen 5 5600X에서 Go 1.27.1로 측정한 결과, 헬퍼는 연산당 7.14ns, 표준 라이브러리 구현은 0.88ns, unsafe 구현은 0.87ns였습니다.

어셈블리를 비교하면 표준 라이브러리 구현과 unsafe 구현은 IPv4 여부를 확인하고 z 값을 바꾸는 짧은 코드로 끝납니다. 반면 헬퍼는 As16()에서 주소를 바이트 배열에 넣고, AddrFrom16()에서 다시 읽습니다. 컴파일러가 이 과정의 바이트 변환과 복사를 없애지 못해 스택 메모리에 값을 저장하고 복사한 뒤 다시 읽습니다.

컴파일러 최적화 실험

첫 시도는 컴파일러의 noding 단계에서 AddrFrom16(ip.As16()) 호출을 구조체 리터럴로 바꾸는 방식입니다. 이 단계에서는 패키지 내부의 비공개 필드에 접근할 수 없지만, 글쓴이는 컴파일러를 수정해 netip.Addr의 주소 필드를 그대로 복사하고 z 필드만 IPv6 값으로 설정하도록 했습니다. 생성된 코드는 네이티브 구현만큼 짧아졌습니다. 다만 noding은 타입 검사가 끝난 코드를 중간 표현으로 옮기는 역할을 맡습니다. 특정 패키지의 내부 구조를 알고 코드를 변형하는 방식은 이 단계의 역할과 맞지 않고 유지하기도 어렵습니다.

다음 시도는 플랫폼에 독립적인 SSA 최적화 단계에서 불필요한 메모리 작업을 없애는 방식입니다. 필요한 변환은 크게 세 가지입니다. Move 뒤에 오는 Load를 원래 주소의 Load로 바꾸고, Store 직후 같은 주소에서 읽는 값은 저장한 값으로 대체하며, 연달아 적용된 두 번의 바이트 스왑을 상쇄합니다. 일부 규칙은 이미 Go에 있었지만, Move를 거친 Load를 처리하는 규칙에는 오프셋이 없는 경우가 빠져 있었습니다.

빠진 규칙을 더했지만 코드가 오히려 나빠졌습니다. 규칙이 memcombine보다 먼저 실행되면서 바이트 배열의 여러 Load가 개별 값으로 전달됐고, memcombine이 이를 하나의 64비트 Load와 바이트 스왑으로 합치지 못했습니다. Go 1.27에서 추가된 Move를 거친 Load 최적화도 이 회귀에 영향을 줬습니다. 글쓴이는 late opt 단계보다 앞서 memcombine을 한 차례 더 실행해 해결했습니다. 수정한 브랜치에서는 헬퍼가 0.89ns로 줄어 네이티브 구현의 0.88ns에 가까워졌습니다. Go 1.26.8의 헬퍼 측정값 6.55ns와 비교하면 86.34% 감소한 결과입니다.

표준 라이브러리 메서드와 후속 과제

글쓴이는 추가 memcombine 실행이 컴파일러에 부담을 더해 제안이 받아들여지기 어렵다고 봅니다. 컴파일러 수정 대신 To6() 메서드를 표준 라이브러리에 추가하는 편이 단순할 수 있다고 보고, 관련 이슈 #54365에서 논의를 이어갈 계획입니다. 글은 Go 컴파일러의 중간 표현과 최적화 순서가 코드 생성 결과에 어떤 영향을 주는지 구체적인 SSA와 어셈블리 사례로 보여줍니다.

원문: Vincent Bernat / 번역·요약: Trawling