在 C# 中超高速解析 IP 地址

Lobsters Hottest 工具

摘要

本文介绍了一种优化方法,用于在 C# 中使用 AVX-512 SIMD 指令解析 IPv4 地址,通过利用 .NET 10 的掩码加载功能,在 UTF-16 字符串中实现高性能。

<p><a href="https://lobste.rs/s/fboohk/parsing_ip_addresses_c_at_crazy_speeds">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/20 08:45

# C# 中超高速解析 IP 地址 来源:https://lemire.me/blog/2026/08/19/parsing-ip-addresses-in-c-at-crazy-speeds/ 我们都熟悉像 `192.168.0.1` 这样的 IP 地址。它们通常写成四个介于 0 到 255 之间的数字,用点分隔。在 C# 中,你可以使用标准库的 `IPAddress.TryParse` 来解析它们。 严谨的人会迅速指出,IP 地址可以有不同的形式:可以是 IPv6 或 IPv4,并且 IPv4 地址有许多奇怪的写法。但为了性能优化的目的,我们关注的是常见情况。常见情况是像 `192.168.0.1` 或 `12.121.244.111` 这样的字符串。 我们的处理器支持数据并行性,这意味着它们拥有可以同时处理多个字节(至少 16 字节,有时更多)的指令(称为 SIMD)。几年前,我展示了你可以用 SIMD 来解析 IPv4 地址 (https://lemire.me/blog/2023/06/08/parsing-ip-addresses-crazily-fast/)。我一直使用 AVX-512 (https://github.com/lemire/simdip) 这个近年 x64(AMD/Intel)处理器支持的指令集来重新审视这个想法。我预计不久的将来所有 Intel 和 AMD 处理器都会对 AVX-512 有很好的支持,并且这在服务器处理器和最新的 AMD 处理器上已经是现实了。 所以我想,我们能在 C# 中实现吗?有时人们会惊讶我关注 C#。那难道不是更多微软的垃圾吗?不,完全不是。C# 和 .NET 是非常合理、可移植的系统。而且你可以在 C# 中编写快速代码。我有两个优化库,希望有一天能被微软 .NET 团队纳入标准 .NET 库:一个用于内部验证 Unicode 字符串的优化函数 `Utf8Utility.GetPointerToFirstInvalidByte`(在 SimdUnicode 库中 https://github.com/simdutf/SimdUnicode),以及一个快速的 base64 解码库 (https://github.com/simdutf/SimdBase64)。我喜欢使用 .NET C#。 从 .NET 10 开始,我们有了 AVX-512 支持,包括掩码加载。什么是掩码加载,为什么重要?假设我给你一个不超过 16 字节但可能更短的字符串。如果你将数据加载到 SIMD 寄存器中,通常需要加载整个寄存器宽度(比如 8、16、32、64 字节)。那么当你无法加载完整宽度时怎么办?你可以填充输入字符串或使用其他技巧,但这会变得很乱。一个很好的方法是使用掩码加载:你加载完整的寄存器(比如 16 字节),但通过掩码指示要从内存加载哪些字节。例如,如果你使用 `0b10011` 作为掩码,则仅从内存加载第一、第二和第五个字节。这使得你可以用一个介于 0 到 16 字节的字符串初始化一个 16 字节寄存器,同时永远不会读取字符串超出的部分。 如果你想知道更多,我有一篇文章名为《使用掩码加载和存储的现代向量编程》(https://lemire.me/blog/2022/11/08/modern-vector-programming-with-masked-loads-and-stores/)。 更棘手的是,C# 和 Java、JavaScript 一样,默认使用 UTF-16,这意味着每个字符(即使是像 `A` 或 `1` 这样的 ASCII 字符)都占用两个字节。ASCII 码点值占据 16 位字的最低有效位。因此我们需要做的是从 32 字节的输入中有选择地加载,然后丢弃不必要的零字节。其核心在 C# 中如下所示。 ```csharp unsafe bool TryParseAvx512(ReadOnlySpan<char> s, out uint ip) { int len = s.Length; fixed (char* cp = s) { // 下面两行是一个技巧,只加载前 len 个字符 Vector256<ushort> charMask = Vector256.LessThan(CharLaneIndex, Vector256.Create((ushort)len)); Vector256<ushort> chars = Avx512BW.VL.MaskLoad((ushort*)cp, charMask, Vector256.Create((ushort)'0')); // 检查是否全部为 ASCII,否则不是 IP! if (Avx512BW.VL.CompareGreaterThan(chars, Vector256.Create((ushort)0x7F)).ExtractMostSignificantBits() != 0) { return false; } // 现在我们以 ASCII 形式在 16 字节寄存器中得到了地址 Vector128<byte> str = Avx512BW.VL.ConvertToVector128Byte(chars); // ... } } ``` 这看起来有点难以阅读,但这没关系。大多数人永远不需要担心这样的代码。 然后我们使用一个有点巧妙的技巧:定位点,并利用点的位置只有 81 种可能这一事实。然后我们移动字节,执行点积并验证。这与 C++ 代码中的例程相同。它并不简单,但我正在撰写一篇正式论文来记录所使用的技巧。 严谨的人会说:等等,还有其他方式写 IP 地址!!!好吧,我们用一个后备方案来处理它们,像这样。 ```csharp if (TryParseAvx512(s, out uint ip)) { address = new IPAddress(ip); return true; } return IPAddress.TryParse(s, out address); ``` 如果处理器不支持 AVX-512 怎么办?C# 让这变得非常容易。你只需要用一个 if 来守护它: ```csharp if (Avx512BW.VL.IsSupported) { ... } ``` 为了进行基准测试,我生成了 10,000 个随机的 32 位地址,并将生成的字符串解析了 2000 万次,每次构造一个 `IPAddress`。在一个相对较新的 Intel 处理器(Intel Xeon Gold 6548N, Emerald Rapids)上运行 .NET 10,我得到以下结果。 | 函数 | 纳秒/地址 | 百万地址/秒 | |---|---|---| | `IPAddress.TryParse` | 45.3 | 22.1 | | AVX-512 + 后备方案 | 14.1 | 71.1 | 所以 AVX-512 方法大约比标准库快 **三倍**。我的例程本身不需要十四纳秒;存在其他开销。 像往常一样,C# 源码已发布 (https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/tree/master/2026/08/19)。

相似文章

ARM处理器上匹配字符的最快方法?

Lobsters Hottest

本文探讨了在ARM处理器上使用SIMD指令进行字符匹配的最快方法,比较了传统的NEON方法与现代ARM芯片(如AWS Graviton4、Google Axion等)上可用的较新SVE2能力。

使用SIMD加速std::copy_if

Lobsters Hottest

一篇博文,分析和实现了在AMD Zen 4上使用AVX-512指令的SIMD加速版本的std::copy_if,并进行了性能分析和与编译器自动向量化的对比。