Hacking the Go compiler to efficiently map IPv4 to IPv6

Hacking the Go compiler to efficiently map IPv4 to IPv6

破解 Go 编译器:高效实现 IPv4 到 IPv6 的映射

netip.Addr features an Unmap() method returning the unwrapped IPv4 contained in an IPv4-mapped IPv6 address: from ::ffff:203.0.113.10 or ::ffff:cb00:710a, it returns 203.0.113.10. There is no Map() or To6() method for the reverse direction. Such a method is trivial to implement, but Go maintainers have rejected it on the grounds that users should write netip.AddrFrom16(ip.As16()) and let the compiler optimize it. Today, this pattern is eight times slower than a native method. How can we teach the compiler to optimize this sequence?

netip.Addr 提供了一个 Unmap() 方法,用于返回 IPv4 映射的 IPv6 地址中包含的原始 IPv4 地址:例如,从 ::ffff:203.0.113.10 或 ::ffff:cb00:710a 中,它会返回 203.0.113.10。然而,反向操作并没有对应的 Map() 或 To6() 方法。实现这样一个方法非常简单,但 Go 维护者拒绝了该提议,理由是用户应该编写 netip.AddrFrom16(ip.As16()) 并让编译器进行优化。目前,这种写法比原生方法慢了八倍。我们该如何教编译器优化这一序列呢?

The alternatives

替代方案

Let’s explore three ways to implement the map semantics for netip.Addr. My favorite is to add it to the Go standard library. Go maintainers prefer a small external helper chaining netip.AddrFrom16() and netip.Addr.As16(), hoping the compiler eventually optimizes it. The unsafe package opens a third path, with the same performance as the first solution.

让我们探讨三种为 netip.Addr 实现映射语义的方法。我最倾向于将其添加到 Go 标准库中。Go 维护者则倾向于使用一个小的外部辅助函数来串联 netip.AddrFrom16() 和 netip.Addr.As16(),并希望编译器最终能对其进行优化。unsafe 包则开启了第三条路径,其性能与第一种方案相同。

Modifying the Go standard library

修改 Go 标准库

Internally, netip.Addr stores any IP address as a 128-bit value with an extra field z to encode the family and the zone. Implementing the reverse direction inside the Go standard library is trivial: we set the z field to z6noz if the address is IPv4.

在内部,netip.Addr 将任何 IP 地址存储为一个 128 位的值,并带有一个额外的 z 字段来编码地址族和区域。在 Go 标准库内部实现反向转换非常简单:如果地址是 IPv4,我们只需将 z 字段设置为 z6noz 即可。

// To6 maps an IPv4 address to an IPv4-mapped IPv6 address. 
// It returns an IPv6 address unmodified.
func (ip Addr) To6() Addr {
    if ip.Is4() {
        ip.z = z6noz
    }
    return ip
}

As a helper

作为辅助函数

We can’t access the z field from outside the net/netip package. Instead, we build a small helper around the netip.AddrFrom16(ip.As16()) pattern.

我们无法在 net/netip 包之外访问 z 字段。因此,我们围绕 netip.AddrFrom16(ip.As16()) 模式构建了一个小的辅助函数。

func AddrTo6(ip netip.Addr) netip.Addr {
    if ip.Is4() {
        ip = netip.AddrFrom16(ip.As16())
    }
    return ip
}

As an unsafe function

作为 unsafe 函数

Another solution uses the unsafe package to alter the Addr struct through a proxy with the same memory layout.

另一种解决方案是使用 unsafe 包,通过一个具有相同内存布局的代理结构体来修改 Addr 结构体。

func AddrTo6(ip netip.Addr) netip.Addr {
    if !ip.Is4() {
        return ip
    }
    (*addrProxy)(unsafe.Pointer(&ip)).z = netipZ6noz
    return ip
}

Benchmarks

基准测试

On my computer, with Go 1.27.1, the standard library solution costs 0.88 ns per operation, while the solution favored by Go maintainers costs 7.14 ns. The unsafe solution matches the performance of the first one.

在我的电脑上,使用 Go 1.27.1 版本,标准库方案每次操作耗时 0.88 纳秒,而 Go 维护者青睐的方案耗时 7.14 纳秒。unsafe 方案的性能与第一种方案持平。

Methodsec/op
AddrTo6/safe7.137n ± 0%
AddrTo6/unsafe0.8682n ± 2%
AddrTo6/builtin0.8775n ± 2%

Assembly code

汇编代码

Let’s check the assembly code the compiler generates for each solution. The one built into the standard library is highly efficient, using only a few instructions to check the z field and update it. The helper solution has far more instructions because it packs the IP address into an array, copies it, then unpacks it. We can already guess the pattern to optimize: the code packs the IP address into an array, copies it, then unpacks it.

让我们检查一下编译器为每种方案生成的汇编代码。内置于标准库的方案非常高效,仅需几条指令即可检查并更新 z 字段。而辅助函数方案则包含更多的指令,因为它需要将 IP 地址打包到数组中、进行复制,然后再解包。我们已经可以猜到需要优化的模式:代码将 IP 地址打包进数组,复制,然后再解包。