Skip to content

Commit d2c9fab

Browse files
authored
Merge pull request #108 from oschwald/greg/perf-improv
Performance improvements
2 parents 3a79c76 + 98a777c commit d2c9fab

8 files changed

Lines changed: 391 additions & 143 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Change Log
22

3+
## 0.27.2 - 2026-02-14
4+
5+
- Performance improvement: Faster lookups and record decoding in common paths,
6+
including control-byte header parsing and integer decoding.
7+
- Performance improvement: Faster string decoding for ASCII-only values by
8+
using a dedicated ASCII fast path.
9+
- Performance improvement: Reduced release-build overhead by compiling out
10+
debug/trace logging calls.
11+
- Fixed: Truncated or corrupt data in control-byte headers now returns a
12+
decoding error instead of panicking.
13+
314
## 0.27.1 - 2025-12-18
415

516
- Performance improvement: Skipped UTF-8 validation for map keys during

Cargo.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "maxminddb"
3-
version = "0.27.1"
3+
version = "0.27.2"
44
authors = [ "Gregory J. Oschwald <oschwald@gmail.com>" ]
55
description = "Library for reading MaxMind DB format used by GeoIP2 and GeoLite2"
66
readme = "README.md"
@@ -27,7 +27,7 @@ name = "maxminddb"
2727

2828
[dependencies]
2929
ipnetwork = "0.21.1"
30-
log = "0.4"
30+
log = { version = "0.4", features = ["release_max_level_info"] }
3131
serde = { version = "1.0", features = ["derive"] }
3232
memchr = "2.4"
3333
memmap2 = { version = "0.9.0", optional = true }
@@ -37,10 +37,13 @@ thiserror = "2.0"
3737
[dev-dependencies]
3838
env_logger = "0.11"
3939
criterion = "0.8"
40-
fake = "4.0"
4140
rayon = "1.5"
4241
serde_json = "1.0"
4342

4443
[[bench]]
4544
name = "lookup"
4645
harness = false
46+
47+
[[bench]]
48+
name = "serde_usage"
49+
harness = false

benches/common.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use std::net::{IpAddr, Ipv4Addr};
2+
3+
// Generate `count` IPv4 addresses from a deterministic LCG stream.
4+
#[must_use]
5+
pub fn generate_ipv4(count: u64) -> Vec<IpAddr> {
6+
let mut ips = Vec::with_capacity(count as usize);
7+
let mut state = 0x4D59_5DF4_D0F3_3173_u64;
8+
for _ in 0..count {
9+
state = state
10+
.wrapping_mul(6_364_136_223_846_793_005)
11+
.wrapping_add(1_442_695_040_888_963_407);
12+
let ip = Ipv4Addr::new(
13+
(state >> 24) as u8,
14+
(state >> 32) as u8,
15+
(state >> 40) as u8,
16+
(state >> 48) as u8,
17+
);
18+
ips.push(IpAddr::V4(ip));
19+
}
20+
ips
21+
}

benches/lookup.rs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,16 @@
11
#[macro_use]
22
extern crate criterion;
3-
extern crate fake;
43
extern crate maxminddb;
54
extern crate rayon;
65

76
use criterion::Criterion;
8-
use fake::faker::internet::raw::IPv4;
9-
use fake::locales::EN;
10-
use fake::Fake;
117
use maxminddb::geoip2;
128
use rayon::prelude::*;
139

1410
use std::net::IpAddr;
15-
use std::str::FromStr;
1611

17-
// Generate `count` IPv4 addresses
18-
#[must_use]
19-
pub fn generate_ipv4(count: u64) -> Vec<IpAddr> {
20-
let mut ips = Vec::new();
21-
for _i in 0..count {
22-
let val: String = IPv4(EN).fake();
23-
let ip: IpAddr = FromStr::from_str(&val).unwrap();
24-
ips.push(ip);
25-
}
26-
ips
27-
}
12+
mod common;
13+
use common::generate_ipv4;
2814

2915
// Single-threaded
3016
pub fn bench_maxminddb<T>(ips: &[IpAddr], reader: &maxminddb::Reader<T>)
@@ -81,7 +67,7 @@ pub fn criterion_par_benchmark(c: &mut Criterion) {
8167
criterion_group! {
8268
name = benches;
8369
config = Criterion::default()
84-
.sample_size(10);
70+
.sample_size(20);
8571

8672
targets = criterion_benchmark, criterion_par_benchmark
8773
}

benches/serde_usage.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use criterion::{criterion_group, criterion_main, Criterion};
2+
use maxminddb::geoip2;
3+
use maxminddb::{LookupResult, PathElement, Reader};
4+
use std::hint::black_box;
5+
6+
use std::net::IpAddr;
7+
8+
mod common;
9+
use common::generate_ipv4;
10+
11+
const DB_FILE: &str = "GeoLite2-City.mmdb";
12+
13+
fn cache_lookups<'a, T>(ips: &[IpAddr], reader: &'a Reader<T>) -> Vec<LookupResult<'a, T>>
14+
where
15+
T: AsRef<[u8]>,
16+
{
17+
ips.iter()
18+
.map(|ip| reader.lookup(*ip).unwrap())
19+
.filter(|r| r.has_data())
20+
.collect()
21+
}
22+
23+
fn bench_lookup_only<T>(ips: &[IpAddr], reader: &Reader<T>)
24+
where
25+
T: AsRef<[u8]>,
26+
{
27+
for ip in ips {
28+
let result = reader.lookup(*ip).unwrap();
29+
black_box(result.has_data());
30+
}
31+
}
32+
33+
fn bench_decode_city_only<T>(results: &[LookupResult<'_, T>])
34+
where
35+
T: AsRef<[u8]>,
36+
{
37+
for result in results {
38+
let city: Option<geoip2::City<'_>> = result.decode().unwrap();
39+
black_box(city);
40+
}
41+
}
42+
43+
fn bench_decode_country_only<T>(results: &[LookupResult<'_, T>])
44+
where
45+
T: AsRef<[u8]>,
46+
{
47+
for result in results {
48+
let country: Option<geoip2::Country<'_>> = result.decode().unwrap();
49+
black_box(country);
50+
}
51+
}
52+
53+
fn bench_decode_path_country_iso<T>(results: &[LookupResult<'_, T>])
54+
where
55+
T: AsRef<[u8]>,
56+
{
57+
let path = [PathElement::Key("country"), PathElement::Key("iso_code")];
58+
for result in results {
59+
let value: Option<&str> = result.decode_path(&path).unwrap();
60+
black_box(value);
61+
}
62+
}
63+
64+
fn bench_decode_path_city_name<T>(results: &[LookupResult<'_, T>])
65+
where
66+
T: AsRef<[u8]>,
67+
{
68+
let path = [
69+
PathElement::Key("city"),
70+
PathElement::Key("names"),
71+
PathElement::Key("en"),
72+
];
73+
for result in results {
74+
let value: Option<&str> = result.decode_path(&path).unwrap();
75+
black_box(value);
76+
}
77+
}
78+
79+
pub fn serde_usage_benchmark(c: &mut Criterion) {
80+
let ips = generate_ipv4(100);
81+
82+
#[cfg(not(feature = "mmap"))]
83+
let reader = Reader::open_readfile(DB_FILE).unwrap();
84+
#[cfg(feature = "mmap")]
85+
// SAFETY: The benchmark database file will not be modified during the benchmark.
86+
let reader = unsafe { Reader::open_mmap(DB_FILE) }.unwrap();
87+
88+
let cached_results = cache_lookups(&ips, &reader);
89+
90+
c.bench_function("serde_usage/lookup_only", |b| {
91+
b.iter(|| bench_lookup_only(&ips, &reader))
92+
});
93+
c.bench_function("serde_usage/decode_city_only", |b| {
94+
b.iter(|| bench_decode_city_only(&cached_results))
95+
});
96+
c.bench_function("serde_usage/decode_country_only", |b| {
97+
b.iter(|| bench_decode_country_only(&cached_results))
98+
});
99+
c.bench_function("serde_usage/decode_path_country_iso", |b| {
100+
b.iter(|| bench_decode_path_country_iso(&cached_results))
101+
});
102+
c.bench_function("serde_usage/decode_path_city_name", |b| {
103+
b.iter(|| bench_decode_path_city_name(&cached_results))
104+
});
105+
}
106+
107+
criterion_group! {
108+
name = benches;
109+
config = Criterion::default().sample_size(20);
110+
targets = serde_usage_benchmark
111+
}
112+
criterion_main!(benches);

0 commit comments

Comments
 (0)