快速解析IP地址(便携式,无需SIMD魔法)
大多数程序员都熟悉IP地址。它们由四个介于0到255之间的数字组成,以点分隔:例如 192.168.0.1。从某种意义上说,这是一种表示32位整数的复杂方式。现代版本的IP地址是IPv6,通常用方括号包围,但根据我的经验,它不太常见。
使用花哨的技术,你可以用少至50条指令来解析IP地址。但这有点复杂,且不一定可移植。如果你想要高速度,但又不想做太多工作或使用专门的库呢?你可以尝试自己编写。但既然我是一个文明程序员,我直接让我最喜欢的AI为我写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// 解析从'p'开始的IPv4地址。
// p : 起始指针,pend: 字符串的末尾
std::expected<uint32_t, parse_error> parse_manual(const char *p, const char *pend) {
uint32_t ip = 0;
int octets = 0;
while (p < pend && octets < 4) {
uint32_t val = 0;
const char *start = p;
while (p < pend && *p >= '0' && *p <= '9') {
val = val * 10 + (*p - '0');
if (val > 255) {
return std::unexpected(invalid_format);
}
p++;
}
if (p == start || (p - start > 1 && *start == '0')) {
return std::unexpected(invalid_format);
}
ip = (ip << 8) | val;
octets++;
if (octets < 4) {
if (p == pend || *p != '.') {
return std::unexpected(invalid_format);
}
p++; // 跳过点
}
}
if (octets == 4 && p == pend) {
return ip;
} else {
return std::unexpected(invalid_format);
}
}
|
我立刻明白这个函数没有达到可能的最快速度。然后我让AI利用每个数字由一到三位组成的事实来改进结果。我得到了以下合理的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
std::expected<uint32_t, parse_error> parse_manual_unrolled(const char *p, const char *pend) {
uint32_t ip = 0;
int octets = 0;
while (p < pend && octets < 4) {
uint32_t val = 0;
if (p < pend && *p >= '0' && *p <= '9') {
val = (*p++ - '0');
if (p < pend && *p >= '0' && *p <= '9') {
if (val == 0) {
return std::unexpected(invalid_format);
}
val = val * 10 + (*p++ - '0');
if (p < pend && *p >= '0' && *p <= '9') {
val = val * 10 + (*p++ - '0');
if (val > 255) {
return std::unexpected(invalid_format);
}
}
}
} else {
return std::unexpected(parse_error::invalid_format);
}
ip = (ip << 8) | val;
octets++;
if (octets < 4) {
if (p == pend || *p != '.') {
return std::unexpected(invalid_format);
}
p++; // 跳过点
}
}
if (octets == 4 && p == pend) {
return ip;
} else {
return std::unexpected(invalid_format);
}
}
|
AI干得漂亮!
在C++中,我们有解析数字的标准函数(std::from_chars),它可以显著简化代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
std::expected<uint32_t, parse_error> parse_ip(const char *p, const char *pend) {
const char *current = p;
uint32_t ip = 0;
for (int i = 0; i < 4; ++i) {
uint8_t value;
auto r = std::from_chars(current, pend, value);
if (r.ec != std::errc()) {
return std::unexpected(invalid_format);
}
current = r.ptr;
ip = (ip << 8) | value;
if (i < 3) {
if (current == pend || *current++ != '.') {
return std::unexpected(invalid_format);
}
}
}
return ip;
}
|
你也可以使用 fast_float 库作为 std::from_chars 的替代品。最新版本的 fast_float 得益于 Shikhar Soni(以及 Pavel Novikov 的修复)的贡献,实现了更快的8位整数解析。
我为这个问题编写了一个基准测试。让我们首先看看在 Apple M4 处理器(4.5 GHz)上使用 LLVM 17 的结果。
| 函数 |
指令数/IP |
纳秒/IP |
| manual |
185 |
6.2 |
| manual (unrolled) |
114 |
3.3 |
| from_chars |
381 |
14 |
| fast_float |
181 |
7.2 |
让我们用 GCC 12 和 Intel Ice Lake 处理器(3.2 GHz)试试。
| 函数 |
指令数/IP |
纳秒/IP |
| manual |
219 |
30 |
| manual (unrolled) |
154 |
24 |
| from_chars |
220 |
29 |
| fast_float |
211 |
18 |
最后,让我们用中国的龙芯 3A6000 处理器(2.5 GHz)和 LLVM 21 试试。
| 函数 |
指令数/IP |
纳秒/IP |
| manual |
187 |
29 |
| manual (unrolled) |
109 |
21 |
| from_chars |
191 |
39 |
| fast_float |
193 |
27 |
对 fast_float 库的优化工作取得了成效。这种差异在 x64 处理器上尤其明显。
在我这个小实验中,另一个有趣的点是,我能够以相对较少的个人努力就让AI生成了更快的代码。我确实需要“引导”AI。这是否意味着我可以退休了?还没到那一步。但我很高兴我能更快地获得良好的参考基线,这使我能够更好地将工作重点放在关键之处。
参考:fast_float C++ 库是一个快速数字解析库,是 GCC 和主要网络浏览器的一部分。
Daniel Lemire, “Parsing IP addresses quickly (portably, without SIMD magic),” in Daniel Lemire’s blog, December 27, 2025, https://lemire.me/blog/2025/12/27/parsing-ip-addresses-quickly-portably-without-simd-magic/.