Skip to content

Commit f894eb8

Browse files
committed
Added benchmarks vs S2, Boost.Geometry, GeographicLib
1 parent 3123b5f commit f894eb8

18 files changed

Lines changed: 1283 additions & 2 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
build/
2+
build-*/
23

34
# Compiled Object files
45
*.slo

CMakeLists.txt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ else()
88
endif()
99

1010
include(CMakeDependentOption)
11-
option(GEO_UTILS_CPP_BUILD_TESTS "Build geo-utils-cpp tests" ${_geo_utils_cpp_is_top_level})
12-
option(GEO_UTILS_CPP_BUILD_EXAMPLES "Build geo-utils-cpp examples" ${_geo_utils_cpp_is_top_level})
11+
option(GEO_UTILS_CPP_BUILD_TESTS "Build geo-utils-cpp tests" ${_geo_utils_cpp_is_top_level})
12+
option(GEO_UTILS_CPP_BUILD_EXAMPLES "Build geo-utils-cpp examples" ${_geo_utils_cpp_is_top_level})
13+
option(GEO_UTILS_CPP_BUILD_BENCHMARKS "Build geo-utils-cpp benchmarks" OFF)
1314
cmake_dependent_option(GEO_UTILS_CPP_ENABLE_COVERAGE
1415
"Enable gcov coverage instrumentation (GCC/Clang only)" OFF
1516
"GEO_UTILS_CPP_BUILD_TESTS" OFF)
@@ -98,6 +99,11 @@ if(GEO_UTILS_CPP_BUILD_EXAMPLES)
9899
add_subdirectory(examples)
99100
endif()
100101

102+
# Benchmarks (opt-in: pulls Google Benchmark and optionally S2/Boost/GeographicLib)
103+
if(GEO_UTILS_CPP_BUILD_BENCHMARKS)
104+
add_subdirectory(benchmarks)
105+
endif()
106+
101107
# Installation
102108
include(GNUInstallDirs)
103109

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ int main() {
121121
}
122122
```
123123

124+
## Benchmarks
125+
126+
`geo-utils-cpp` is a near-zero-overhead wrapper over the math itself, with a
127+
tiny disk footprint thanks to header-only + zero dependencies.
128+
129+
| Library | `distance_between` (M pairs/s) | `contains` (poly N=100, M qps) | Install size |
130+
| -------------------- | -----------------------------: | -----------------------------: | ------------: |
131+
| **geo-utils-cpp** | **36.7** | **2.17** | **32 KB** |
132+
| naive haversine | 37.0 || 0 |
133+
| S2 Geometry | 14.3 | 18.0 | 32.8 MB |
134+
| Boost.Geometry | 40.0 | 0.23 | 12.3 MB |
135+
| GeographicLib | 1.2 | no native PIP | 4.6 MB |
136+
137+
Apple M1 · clang 17 · `-O2 -DNDEBUG`. We're tied with hand-written haversine
138+
on raw math, ~7× faster than Boost.Geometry on point-in-polygon, and have a
139+
**144–1000× smaller install footprint** than the alternatives. S2 wins on
140+
large-polygon containment thanks to spatial indexing.
141+
142+
See [docs/benchmarks.md](docs/benchmarks.md) for the full methodology, all
143+
operations, and a discussion of when to reach for each library.
144+
124145
## API Reference
125146

126147
See [docs/api.md](docs/api.md) for the full API reference.

benchmarks/CMakeLists.txt

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Benchmarks for geo-utils-cpp.
2+
#
3+
# Build with:
4+
# cmake -B build -DGEO_UTILS_CPP_BUILD_BENCHMARKS=ON
5+
# cmake --build build --target bench_all
6+
#
7+
# Google Benchmark is fetched automatically. S2, Boost.Geometry, and
8+
# GeographicLib are looked up via find_package — missing competitors are
9+
# skipped with a status message instead of causing a hard error.
10+
11+
include(FetchContent)
12+
13+
# --- Google Benchmark -------------------------------------------------------
14+
15+
set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
16+
set(BENCHMARK_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
17+
set(BENCHMARK_INSTALL_DOCS OFF CACHE BOOL "" FORCE)
18+
set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE)
19+
20+
FetchContent_Declare(
21+
google_benchmark
22+
URL https://github.com/google/benchmark/archive/v1.8.4.tar.gz
23+
DOWNLOAD_EXTRACT_TIMESTAMP ON
24+
)
25+
FetchContent_MakeAvailable(google_benchmark)
26+
27+
# --- Shared infrastructure --------------------------------------------------
28+
29+
add_library(geo_utils_cpp_bench_common INTERFACE)
30+
target_include_directories(geo_utils_cpp_bench_common INTERFACE
31+
${CMAKE_CURRENT_SOURCE_DIR}/common
32+
)
33+
target_compile_features(geo_utils_cpp_bench_common INTERFACE cxx_std_17)
34+
35+
# Aggregator target: depend on every bench that ends up being built.
36+
add_custom_target(bench_all)
37+
38+
function(_geo_utils_cpp_add_bench name source)
39+
add_executable(bench_${name} ${source})
40+
target_link_libraries(bench_${name} PRIVATE
41+
geo::utils
42+
geo_utils_cpp_bench_common
43+
benchmark::benchmark_main
44+
${ARGN}
45+
)
46+
add_dependencies(bench_all bench_${name})
47+
endfunction()
48+
49+
# --- geo-utils-cpp itself + naive haversine baseline ------------------------
50+
51+
_geo_utils_cpp_add_bench(geo_utils speed/bench_geo_utils.cpp)
52+
_geo_utils_cpp_add_bench(naive speed/bench_naive.cpp)
53+
54+
# --- Optional: S2 Geometry --------------------------------------------------
55+
#
56+
# vcpkg port name: `s2geometry`. Homebrew formula: `s2geometry`. The CMake
57+
# package is conventionally exported as `s2`.
58+
find_package(s2 QUIET)
59+
if(s2_FOUND)
60+
_geo_utils_cpp_add_bench(s2 speed/bench_s2.cpp s2::s2)
61+
message(STATUS "geo-utils-cpp benchmarks: S2 found — bench_s2 enabled.")
62+
else()
63+
message(STATUS "geo-utils-cpp benchmarks: S2 not found — bench_s2 skipped. "
64+
"Install via 'vcpkg install s2geometry' or 'brew install s2geometry'.")
65+
endif()
66+
67+
# --- Optional: Boost.Geometry -----------------------------------------------
68+
#
69+
# Boost.Geometry is header-only. CMake 3.30 deprecates the FindBoost module in
70+
# favour of Boost's exported BoostConfig.cmake; we prefer CONFIG and fall back
71+
# to the legacy module so this works across CMake 3.14+.
72+
if(POLICY CMP0167)
73+
cmake_policy(SET CMP0167 NEW)
74+
endif()
75+
76+
find_package(Boost 1.70 QUIET CONFIG)
77+
if(NOT Boost_FOUND)
78+
find_package(Boost 1.70 QUIET)
79+
endif()
80+
81+
if(Boost_FOUND)
82+
# Different Boost versions expose the header set as Boost::headers (>=1.71),
83+
# Boost::boost (older), or only via Boost_INCLUDE_DIRS.
84+
if(TARGET Boost::headers)
85+
_geo_utils_cpp_add_bench(boost speed/bench_boost.cpp Boost::headers)
86+
elseif(TARGET Boost::boost)
87+
_geo_utils_cpp_add_bench(boost speed/bench_boost.cpp Boost::boost)
88+
else()
89+
_geo_utils_cpp_add_bench(boost speed/bench_boost.cpp)
90+
target_include_directories(bench_boost PRIVATE ${Boost_INCLUDE_DIRS})
91+
endif()
92+
message(STATUS "geo-utils-cpp benchmarks: Boost.Geometry found — bench_boost enabled.")
93+
else()
94+
message(STATUS "geo-utils-cpp benchmarks: Boost not found — bench_boost skipped. "
95+
"Install via 'vcpkg install boost-geometry' or 'brew install boost'.")
96+
endif()
97+
98+
# --- Optional: GeographicLib ------------------------------------------------
99+
#
100+
# Recent GeographicLib versions export the target `GeographicLib::GeographicLib`.
101+
# Older versions exposed `${GeographicLib_LIBRARIES}` instead — we accept both.
102+
find_package(GeographicLib QUIET)
103+
if(GeographicLib_FOUND)
104+
if(TARGET GeographicLib::GeographicLib)
105+
_geo_utils_cpp_add_bench(geographiclib speed/bench_geographiclib.cpp
106+
GeographicLib::GeographicLib)
107+
else()
108+
_geo_utils_cpp_add_bench(geographiclib speed/bench_geographiclib.cpp
109+
${GeographicLib_LIBRARIES})
110+
target_include_directories(bench_geographiclib PRIVATE ${GeographicLib_INCLUDE_DIRS})
111+
endif()
112+
message(STATUS "geo-utils-cpp benchmarks: GeographicLib found — bench_geographiclib enabled.")
113+
else()
114+
message(STATUS "geo-utils-cpp benchmarks: GeographicLib not found — bench_geographiclib skipped. "
115+
"Install via 'vcpkg install geographiclib' or 'brew install geographiclib'.")
116+
endif()

benchmarks/README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Benchmarks
2+
3+
This directory measures `geo-utils-cpp` against three popular C++ geometry
4+
libraries on two axes:
5+
6+
1. **Speed** — Google Benchmark micro-benchmarks for `distance`, `heading`,
7+
`contains`, `area`, and `path_length`.
8+
2. **Disk footprint** — stripped binary size of a minimal consumer plus the
9+
on-disk install size of each library.
10+
11+
The benchmarks are **not built by default** and **not run in CI**. Set
12+
`-DGEO_UTILS_CPP_BUILD_BENCHMARKS=ON` to opt in.
13+
14+
For a high-level summary of results and methodology see
15+
[`docs/benchmarks.md`](../docs/benchmarks.md).
16+
17+
## Competitors
18+
19+
| Library | Model | Notes |
20+
| -------------------- | ----------- | ------------------------------------------------- |
21+
| **geo-utils-cpp** | sphere | This library — header-only, zero deps |
22+
| **naive haversine** | sphere | ~30 lines of inline math, no library |
23+
| **S2 Geometry** | sphere | Google's mapping library; depends on abseil |
24+
| **Boost.Geometry** | sphere\* | `cs::spherical_equatorial<degree>` strategy |
25+
| **GeographicLib** | ellipsoid | Karney's iterative geodesic — more accurate, slower; **no native point-in-polygon** |
26+
27+
\* Boost.Geometry can also do ellipsoidal; we use the spherical strategy here
28+
to compare apples-to-apples on the algorithm we ourselves implement.
29+
30+
## Installing competitors
31+
32+
The CMake build looks each library up via `find_package` and *skips* the
33+
benchmark binary for any competitor it can't find — so you only need to
34+
install the libraries you want to compare against.
35+
36+
### Homebrew (macOS / Linux)
37+
38+
```sh
39+
brew install s2geometry boost geographiclib
40+
```
41+
42+
### vcpkg
43+
44+
```sh
45+
vcpkg install s2geometry boost-geometry geographiclib
46+
```
47+
48+
(Then point CMake at the vcpkg toolchain file with
49+
`-DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake`.)
50+
51+
### apt (Debian/Ubuntu)
52+
53+
S2 is not in apt; install from source or vcpkg. The other two:
54+
55+
```sh
56+
sudo apt install libboost-dev libgeographiclib-dev
57+
```
58+
59+
## Building and running speed benchmarks
60+
61+
```sh
62+
cmake -S . -B build-bench \
63+
-DGEO_UTILS_CPP_BUILD_BENCHMARKS=ON \
64+
-DCMAKE_BUILD_TYPE=Release
65+
cmake --build build-bench --target bench_all -j
66+
67+
# Run any one binary:
68+
./build-bench/benchmarks/bench_geo_utils
69+
./build-bench/benchmarks/bench_naive
70+
./build-bench/benchmarks/bench_s2 # if S2 was found
71+
./build-bench/benchmarks/bench_boost # if Boost was found
72+
./build-bench/benchmarks/bench_geographiclib # if GeographicLib was found
73+
```
74+
75+
Each binary supports the standard Google Benchmark flags. To produce a single
76+
combined JSON report:
77+
78+
```sh
79+
for b in build-bench/benchmarks/bench_*; do
80+
"$b" --benchmark_format=json --benchmark_out="$(basename $b).json"
81+
done
82+
```
83+
84+
## Measuring disk footprint
85+
86+
```sh
87+
./benchmarks/size/measure.sh
88+
```
89+
90+
This builds a minimal "distance + point-in-polygon" consumer against every
91+
library it can find, strips the resulting binary, and reports both the
92+
binary size and the on-disk install size of each library. Override compiler
93+
or flags via `CXX=` and `CXXFLAGS=`.
94+
95+
## Methodology notes
96+
97+
- **Same inputs everywhere.** All benchmarks pull data from
98+
`common/random_data.hpp`, which is seeded deterministically.
99+
- **Conversion costs are included** for libraries that don't accept lat/lng
100+
natively (S2 takes `S2Point`; Boost takes its own point type). Pre-converting
101+
outside the timing loop would understate the cost of "I have lat/lng, I want
102+
a distance" — the actual usage pattern.
103+
- **Polygons are pre-built** outside the timing loop because in real code a
104+
geofence is a one-time setup cost.
105+
- **Don't compare GeographicLib's distance/heading head-to-head as "speed".**
106+
It computes a different (more accurate) thing on the WGS84 ellipsoid.
107+
Treat it as a "trade-off" data point.

benchmarks/common/random_data.hpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2026 Aleksandr Kovalko
2+
// Licensed under the Apache License, Version 2.0
3+
//
4+
// Deterministic random data shared by every benchmark binary, so that all
5+
// libraries compete on identical inputs.
6+
7+
#pragma once
8+
9+
#include <cmath>
10+
#include <cstddef>
11+
#include <cstdint>
12+
#include <random>
13+
#include <vector>
14+
15+
#include <geo/latlng.hpp>
16+
17+
namespace geo::bench {
18+
19+
inline constexpr std::uint64_t kSeed = 0xC0FFEEull;
20+
21+
// Returns `n` random points uniformly distributed over the globe. Latitude is
22+
// clamped to [-80, 80] to keep all libraries on safe ground (S2 is fine near
23+
// the poles, but ellipsoidal Inverse can be numerically iffy for nearly
24+
// antipodal pairs).
25+
inline std::vector<LatLng> random_points(std::size_t n, std::uint64_t seed = kSeed) {
26+
std::mt19937_64 rng(seed);
27+
std::uniform_real_distribution<double> lat(-80.0, 80.0);
28+
std::uniform_real_distribution<double> lng(-180.0, 180.0);
29+
std::vector<LatLng> out;
30+
out.reserve(n);
31+
for (std::size_t i = 0; i < n; ++i) {
32+
out.emplace_back(lat(rng), lng(rng));
33+
}
34+
return out;
35+
}
36+
37+
// Returns a regular n-gon centered at (clat, clng) with the given radius
38+
// (degrees). Vertices are emitted counter-clockwise (positive signed area
39+
// in our convention).
40+
inline std::vector<LatLng> regular_polygon(std::size_t n, double clat, double clng, double radius_deg) {
41+
constexpr double kPi = 3.14159265358979323846;
42+
std::vector<LatLng> out;
43+
out.reserve(n);
44+
for (std::size_t i = 0; i < n; ++i) {
45+
double a = 2.0 * kPi * static_cast<double>(i) / static_cast<double>(n);
46+
out.emplace_back(clat + radius_deg * std::cos(a),
47+
clng + radius_deg * std::sin(a));
48+
}
49+
return out;
50+
}
51+
52+
// Query points clustered around the polygon center. Roughly half land
53+
// inside, half outside — exercises both branches of contains().
54+
inline std::vector<LatLng> queries_around(double clat, double clng, double radius_deg,
55+
std::size_t n, std::uint64_t seed = kSeed + 1) {
56+
std::mt19937_64 rng(seed);
57+
std::uniform_real_distribution<double> jitter(-2.0, 2.0);
58+
std::vector<LatLng> out;
59+
out.reserve(n);
60+
for (std::size_t i = 0; i < n; ++i) {
61+
out.emplace_back(clat + radius_deg * jitter(rng),
62+
clng + radius_deg * jitter(rng));
63+
}
64+
return out;
65+
}
66+
67+
} // namespace geo::bench

benchmarks/size/consumer_boost.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Minimal consumer: distance + point-in-polygon, Boost.Geometry.
2+
3+
#include <cstdio>
4+
#include <cstdlib>
5+
6+
#include <boost/geometry.hpp>
7+
#include <boost/geometry/geometries/point.hpp>
8+
#include <boost/geometry/geometries/polygon.hpp>
9+
#include <boost/geometry/strategies/spherical/distance_haversine.hpp>
10+
11+
namespace bg = boost::geometry;
12+
using Pt = bg::model::point<double, 2, bg::cs::spherical_equatorial<bg::degree>>;
13+
using Poly = bg::model::polygon<Pt>;
14+
15+
int main(int argc, char** argv) {
16+
if (argc < 5) return 1;
17+
Pt a(std::atof(argv[2]), std::atof(argv[1])); // (lng, lat)
18+
Pt b(std::atof(argv[4]), std::atof(argv[3]));
19+
Poly poly;
20+
bg::append(poly.outer(), Pt{-74.1, 40.7});
21+
bg::append(poly.outer(), Pt{-74.1, 40.8});
22+
bg::append(poly.outer(), Pt{-74.0, 40.8});
23+
bg::append(poly.outer(), Pt{-74.0, 40.7});
24+
bg::append(poly.outer(), Pt{-74.1, 40.7});
25+
bg::correct(poly);
26+
27+
bg::strategy::distance::haversine<double> hav(6371009.0);
28+
const double d = bg::distance(a, b, hav);
29+
std::printf("%.1f %d\n", d, static_cast<int>(bg::within(a, poly)));
30+
return 0;
31+
}

0 commit comments

Comments
 (0)