27 December 2025 · 5 min
Most programmers are familiar with IP addresses. They take the form
of four numbers between 0 and 255 separated by dots: 192.168.0.1.
In some sense, it is a convoluted way to represent a 32-bit integer.
The modern version of an IP address is IPv6 which is usually surrounded
by square brackets. It is less common in my experience.
Using fancy techniques, you can parse IP addresses with as little as 50 instructions. It is a bit complicated and not necessarily portable.
What if you want high speed without too much work or a specialized library? You can try to roll your own. But since I am civilized programmer, I just asked my favorite AI to write it for me.
// Parse an IPv4 address starting at 'p'.
// p : start pointer, pend: end of the string
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++; // Skip dot
}
}
if (octets == 4 && p == pend) {
return ip;
} else {
return std::unexpected(invalid_format);
}
}
It was immediately clear to me that this function was not as fast as it could be. I then asked the AI to improve the result by using the fact that each number is made of between one and three digits. I got the following reasonable function.
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++; // Skip the dot
}
}
if (octets == 4 && p == pend) {
return ip;
} else {
return std::unexpected(invalid_format);
}
}
Nice work AI!
In C++, we have standard functions to parse numbers (std::from_chars) which can significantly simplify the code.
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;
}
You can also use the fast_float library as a substitute for std::from_chars. The latest version of fast_float has faster 8-bit integer parsing thanks to Shikhar Soni (with a fix by Pavel Novikov).
I wrote a benchmark for this problem. Let us first consider the results using an Apple M4 processors (4.5 GHz) with LLVM 17.
| function | instructions/ip | ns/ip |
|---|---|---|
| manual | 185 | 6.2 |
| manual (unrolled) | 114 | 3.3 |
| from_chars | 381 | 14 |
| fast_float | 181 | 7.2 |
Let us try with GCC 12 and an Intel Ice Lake processor (3.2 GHz) using GCC 12.
| function | instructions/ip | ns/ip |
|---|---|---|
| manual | 219 | 30 |
| manual (unrolled) | 154 | 24 |
| from_chars | 220 | 29 |
| fast_float | 211 | 18 |
And finally, let us try with a Chinese Longsoon 3A6000 processor (2.5 GHz) using LLVM 21.
| function | instructions/ip | ns/ip |
|---|---|---|
| manual | 187 | 29 |
| manual (unrolled) | 109 | 21 |
| from_chars | 191 | 39 |
| fast_float | 193 | 27 |
The optimization work on the fast_float library paid off. The difference is especially striking on the x64 processor.
What is also interesting in my little experiment is that I was able to get the AI to produce faster code with relatively little effort on my part. I did have to ‘guide’ the AI. Does that mean that I can retire? Not yet. But I am happy that I can more quickly get good reference baselines, which allows me to better focus my work where it matters.
Reference: The fast_float C++ library is a fast number parsing library part of GCC and major web browsers.
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/.
[BibTeX]
We can minimize the p<pend checks by adding a fast-path for a potential valid ip:
fastfloat_really_inline tl::expected<uint32_t, parse_error> parse_manual_fast(
const char* p, const char* pend) noexcept {
uint32_t ipv4 = 0;
if (pend – p >= 7) {
for (int i = 0; i < 4; ++i) {
uint32_t val;
char c = *p;
if (c >= ‘0’ && c <= ‘9’) {
val = uint32_t(c – ‘0’);
++p;
} else {
return tl::make_unexpected(parse_error::invalid_format);
}
c = *p;
if (c >= ‘0’ && c <= ‘9’) {
if (val == 0)
return tl::make_unexpected(
parse_error::invalid_format);
val = val * 10u + uint32_t(c – ‘0’);
++p;
c = *p;
if (c >= ‘0’ && c <= ‘9’) {
val = val * 10u + uint32_t(c – ‘0’);
++p;
if (val > 255u)
return tl::make_unexpected(parse_error::invalid_format);
}
}
ipv4 = (ipv4 << 8) | val;
if (i < 3) {
if (*p != ‘.’) return tl::make_unexpected(parse_error::invalid_format);
++p;
}
}
return ipv4;
}
return parse_manual_unrolled(p, pend);
}
Runs faster(Apple M1, Apple clang 14):
parse_ip_std_fromchars : 0.38 GB/s 24.0 Mip/s 41.71 ns/ip 1.96 GHz 81.85 c/ip 289.01 i/ip 5.12 c/b 18.06 i/b 3.53 i/c
parse_ip_fastfloat : 0.81 GB/s 50.7 Mip/s 19.71 ns/ip 1.97 GHz 38.81 c/ip 177.86 i/ip 2.43 c/b 11.12 i/b 4.58 i/c
parse_manual : 0.58 GB/s 36.2 Mip/s 27.66 ns/ip 1.97 GHz 54.37 c/ip 187.83 i/ip 3.40 c/b 11.74 i/b 3.45 i/c
parse_manual_unrolled : 0.92 GB/s 57.3 Mip/s 17.45 ns/ip 1.97 GHz 34.40 c/ip 104.70 i/ip 2.15 c/b 6.54 i/b 3.04 i/c
parse_manual_fast : 1.03 GB/s 64.7 Mip/s 15.47 ns/ip 1.97 GHz 30.52 c/ip 93.90 i/ip 1.91 c/b 5.87 i/b 3.08 i/c
Maybe that’s what you meant to post:
Yes! Couldn’t figure out the formatting.
I guess this runs slightly faster.
OOPS: all your code samples fail for VALID IPv4 addresses written with octal or hexadecimal parts! 192.168.0.1 == 0xc0.0250.000.001
I wrote a standard compliant URL parser that is widely used (par of Node.js, Telegram and so forth). It handles all of IPv4 and IPv6.
We have a paper about it which you may enjoy.
Yagiz Nizipli, Daniel Lemire, Parsing Millions of URLs per Second, Software: Practice and Experience 54 (5), 2024
https://arxiv.org/pdf/2311.10533
Take a VERY CLOSE look at https://url.spec.whatwg.org/#hosts-(domains-and-ip-addresses) where the prefixes 0 for octal and 0x or 0X for hexadecimal IPv4 address parts are specified!
If you believe that my implementation of the WHATWG URL IP address parsing is incorrect or buggy, I strongly urge you to report a reproducible test case. Please file an issue at https://github.com/ada-url/ada
Unfortunately, bugs do happen, but please be precise.
I AM precise: my comment addresses the code you present HERE in this blog post, NOTHING else. The initial AI generated code fails to support octal and hexadecimal numbers which are VALID in IPv4 addresses. But instead to prompt your favorite AI for octal and hexadecimal support you primed it with the WRONG “fact” that each number has 1 to 3 digits: GIGO!
Just in case that you need a second source for a standard: https://pubs.opengroup.org/onlinepubs/9699919799/functions/inet_addr.html specifies “All numbers supplied as parts in IPv4 dotted decimal notation may be decimal, octal, or hexadecimal, as specified in the ISO C standard (that is, a leading 0x or 0X implies hexadecimal; otherwise, a leading ‘0’ implies octal; otherwise, the number is interpreted as decimal).”
Stefan,
I refer you to rule number 1 regarding my terms of use.
https://lemire.me/blog/terms-of-use/
Why not focus on IPv6 addtesses?