Skip to content

Commit 02b854a

Browse files
committed
Refreshed benchmark numbers and tightened apples-to-apples
1 parent bb213ea commit 02b854a

13 files changed

Lines changed: 165 additions & 111 deletions

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,13 @@ jobs:
7171

7272
- name: Test
7373
run: ctest --test-dir build -C Release --output-on-failure
74+
75+
# Smoke-build the dependency-free portion of the benchmarks so that
76+
# changes to benchmarks/CMakeLists.txt or the bench source files can't
77+
# silently break the build. We don't run the benchmarks here — that
78+
# requires installing S2/Boost/GeographicLib and is intentionally
79+
# out-of-scope for CI.
80+
- name: Smoke-build benchmarks (no external deps)
81+
run: |
82+
cmake -S . -B build-bench -DGEO_UTILS_CPP_BUILD_BENCHMARKS=ON -DGEO_UTILS_CPP_BUILD_TESTS=OFF -DGEO_UTILS_CPP_BUILD_EXAMPLES=OFF -DCMAKE_BUILD_TYPE=Release
83+
cmake --build build-bench --config Release --target bench_geo_utils bench_naive

README.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,21 @@ int main() {
126126
`geo-utils-cpp` is a near-zero-overhead wrapper over the math itself, with a
127127
tiny disk footprint thanks to header-only + zero dependencies.
128128

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.
129+
| Library | `distance_between` (M pairs/s) | `contains` (poly N=10, M qps) | Install size |
130+
| -------------------- | -----------------------------: | ----------------------------: | ------------: |
131+
| **geo-utils-cpp** | **39.5** | **15.9** | **32 KB** |
132+
| naive haversine | 37.4 || 0 |
133+
| S2 Geometry | 15.1 | 12.9 | 32.8 MB |
134+
| Boost.Geometry | 38.4 | 1.85| 12.3 MB |
135+
| GeographicLib | 1.2 | no native PIP | 4.6 MB |
136+
137+
Apple M1 · clang 17 · `-O2 -DNDEBUG`. Tied with Boost.Geometry on
138+
per-pair ops (`distance`, `heading`) within noise; ahead on polygon ops
139+
(`area`, `path_length`, `contains` for tiny polygons). Faster than S2 on
140+
`distance`, `heading`, `area`, `path_length`, and on `contains` against
141+
~10-vertex polygons. **S2 wins `contains` from ~100 vertices onward** via
142+
its bounding-rectangle prefilter. **144–1000× smaller install footprint**
143+
than the alternatives. Zero overhead over hand-written haversine.
141144

142145
See [docs/benchmarks.md](docs/benchmarks.md) for the full methodology, all
143146
operations, and a discussion of when to reach for each library.

benchmarks/CMakeLists.txt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE)
1919

2020
FetchContent_Declare(
2121
google_benchmark
22-
URL https://github.com/google/benchmark/archive/v1.8.4.tar.gz
22+
URL https://github.com/google/benchmark/archive/v1.8.4.tar.gz
23+
URL_HASH SHA256=3e7059b6b11fb1bbe28e33e02519398ca94c1818874ebed18e504dc6f709be45
2324
DOWNLOAD_EXTRACT_TIMESTAMP ON
2425
)
2526
FetchContent_MakeAvailable(google_benchmark)
@@ -54,8 +55,9 @@ _geo_utils_cpp_add_bench(naive speed/bench_naive.cpp)
5455
# --- Optional: S2 Geometry --------------------------------------------------
5556
#
5657
# vcpkg port name: `s2geometry`. Homebrew formula: `s2geometry`. The CMake
57-
# package is conventionally exported as `s2`.
58-
find_package(s2 QUIET)
58+
# package is conventionally exported as `s2`, but case-sensitive filesystems
59+
# may also see `S2Config.cmake` from some installs — accept either.
60+
find_package(s2 NAMES s2 S2 QUIET)
5961
if(s2_FOUND)
6062
_geo_utils_cpp_add_bench(s2 speed/bench_s2.cpp s2::s2)
6163
message(STATUS "geo-utils-cpp benchmarks: S2 found — bench_s2 enabled.")
@@ -73,9 +75,9 @@ if(POLICY CMP0167)
7375
cmake_policy(SET CMP0167 NEW)
7476
endif()
7577

76-
find_package(Boost 1.70 QUIET CONFIG)
78+
find_package(Boost 1.71 QUIET CONFIG)
7779
if(NOT Boost_FOUND)
78-
find_package(Boost 1.70 QUIET)
80+
find_package(Boost 1.71 QUIET)
7981
endif()
8082

8183
if(Boost_FOUND)

benchmarks/size/consumer_boost.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ using Poly = bg::model::polygon<Pt>;
1414

1515
int main(int argc, char** argv) {
1616
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]));
17+
char* end;
18+
Pt a(std::strtod(argv[2], &end), std::strtod(argv[1], &end)); // (lng, lat)
19+
Pt b(std::strtod(argv[4], &end), std::strtod(argv[3], &end));
1920
Poly poly;
2021
bg::append(poly.outer(), Pt{-74.1, 40.7});
2122
bg::append(poly.outer(), Pt{-74.1, 40.8});

benchmarks/size/consumer_geo_utils.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
#include <cstdlib>
77
#include <vector>
88

9-
#include <geo/geo.hpp>
9+
#include <geo/poly.hpp>
10+
#include <geo/spherical.hpp>
1011

1112
int main(int argc, char** argv) {
1213
if (argc < 5) return 1;
13-
geo::LatLng a(std::atof(argv[1]), std::atof(argv[2]));
14-
geo::LatLng b(std::atof(argv[3]), std::atof(argv[4]));
14+
char* end;
15+
geo::LatLng a(std::strtod(argv[1], &end), std::strtod(argv[2], &end));
16+
geo::LatLng b(std::strtod(argv[3], &end), std::strtod(argv[4], &end));
1517
std::vector<geo::LatLng> poly = {
1618
{40.7, -74.1}, {40.8, -74.1}, {40.8, -74.0}, {40.7, -74.0},
1719
};

benchmarks/size/consumer_geographiclib.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
int main(int argc, char** argv) {
1313
if (argc < 5) return 1;
1414
double s12 = 0.0;
15+
char* end;
1516
GeographicLib::Geodesic::WGS84().Inverse(
16-
std::atof(argv[1]), std::atof(argv[2]),
17-
std::atof(argv[3]), std::atof(argv[4]),
17+
std::strtod(argv[1], &end), std::strtod(argv[2], &end),
18+
std::strtod(argv[3], &end), std::strtod(argv[4], &end),
1819
s12);
1920
std::printf("%.1f -1\n", s12);
2021
return 0;

benchmarks/size/consumer_naive.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ bool contains(LL p, const std::vector<LL>& poly) {
4949

5050
int main(int argc, char** argv) {
5151
if (argc < 5) return 1;
52-
LL a{std::atof(argv[1]), std::atof(argv[2])};
53-
LL b{std::atof(argv[3]), std::atof(argv[4])};
52+
char* end;
53+
LL a{std::strtod(argv[1], &end), std::strtod(argv[2], &end)};
54+
LL b{std::strtod(argv[3], &end), std::strtod(argv[4], &end)};
5455
std::vector<LL> poly = {
5556
{40.7, -74.1}, {40.8, -74.1}, {40.8, -74.0}, {40.7, -74.0},
5657
};

benchmarks/size/consumer_s2.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
int main(int argc, char** argv) {
1515
if (argc < 5) return 1;
16-
const auto a = S2LatLng::FromDegrees(std::atof(argv[1]), std::atof(argv[2])).ToPoint();
17-
const auto b = S2LatLng::FromDegrees(std::atof(argv[3]), std::atof(argv[4])).ToPoint();
16+
char* end;
17+
const auto a = S2LatLng::FromDegrees(std::strtod(argv[1], &end), std::strtod(argv[2], &end)).ToPoint();
18+
const auto b = S2LatLng::FromDegrees(std::strtod(argv[3], &end), std::strtod(argv[4], &end)).ToPoint();
1819
std::vector<S2Point> verts = {
1920
S2LatLng::FromDegrees(40.7, -74.1).ToPoint(),
2021
S2LatLng::FromDegrees(40.8, -74.1).ToPoint(),

benchmarks/size/measure.sh

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ fi
2222
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2323
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
2424
TMP="$(mktemp -d)"
25+
LOG_DIR="${ROOT_DIR}/build-bench/size-logs"
26+
ANY_FAILURE=0
2527
trap 'rm -rf "${TMP}"' EXIT
2628

2729
is_macos() { [ "$(uname)" = "Darwin" ]; }
@@ -62,11 +64,18 @@ build() {
6264
# name, source, extra_flags...
6365
local name="$1"; shift
6466
local source="$1"; shift
65-
local out="${TMP}/${name// /_}"
66-
if "${CXX}" ${CXXFLAGS} ${EXTRA_LD} "${source}" -o "${out}" "$@" 2>"${TMP}/${name}.log"; then
67+
local safe_name="${name// /_}"
68+
local out="${TMP}/${safe_name}"
69+
local log="${TMP}/${safe_name}.log"
70+
if "${CXX}" ${CXXFLAGS} ${EXTRA_LD} "${source}" -o "${out}" "$@" 2>"${log}"; then
6771
strip "${out}" 2>/dev/null || true
6872
file_size "${out}"
6973
else
74+
# Persist the failure log so the user can diagnose after the trap
75+
# cleans up TMP.
76+
mkdir -p "${LOG_DIR}"
77+
cp "${log}" "${LOG_DIR}/${safe_name}.log"
78+
ANY_FAILURE=1
7079
echo "" # signals "skipped"
7180
fi
7281
}
@@ -154,4 +163,6 @@ echo " - 'Installed' for geo-utils-cpp is the include/ directory (everything"
154163
echo " you ship). For other libraries it's the package install prefix on"
155164
echo " Homebrew (or the geometry subset for Boost) — what the user must"
156165
echo " have on disk to consume the library."
157-
echo " - Build logs (if any) are in: ${TMP} (kept until script exit)"
166+
if [ "${ANY_FAILURE}" = "1" ]; then
167+
echo " - Some builds failed. Diagnostic logs preserved in: ${LOG_DIR}"
168+
fi

benchmarks/speed/bench_boost.cpp

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,19 @@
44
// Boost.Geometry equivalents using `cs::spherical_equatorial<degree>` —
55
// the same sphere-based model we use, so this is a fair head-to-head.
66
//
7+
// Conversion policy (matches bench_s2.cpp):
8+
// * Streams of points: converted INSIDE the timed loop.
9+
// * Long-lived polygon: pre-converted ONCE outside the loop.
10+
//
711
// Coordinate convention reminder: Boost.Geometry expects (lng, lat) order
812
// for spherical_equatorial points.
13+
//
14+
// Strategy note for `contains`: bg::within with cs::spherical_equatorial
15+
// auto-selects strategy::within::spherical_winding, which traces great-circle
16+
// edges and classifies crossings carefully. Our own `geo::contains` defaults
17+
// to rhumb-line edges (geodesic=false), which is significantly cheaper.
18+
// The performance gap on `contains` reflects this algorithmic difference,
19+
// not a Boost misconfiguration.
920

1021
#include <benchmark/benchmark.h>
1122

@@ -85,24 +96,21 @@ static void BM_Boost_Heading(benchmark::State& state) {
8596
}
8697
BENCHMARK(BM_Boost_Heading)->Arg(1000)->Arg(100000);
8798

88-
// --- contains (within) -----------------------------------------------------
99+
// --- contains: polygon converted once; queries converted inside the loop --
89100

90101
static void BM_Boost_Contains(benchmark::State& state) {
91102
const auto poly_ll = geo::bench::regular_polygon(
92103
static_cast<std::size_t>(state.range(0)), 40.0, -74.0, 5.0);
93104
const Polygon poly = to_boost_polygon(poly_ll);
94105

95106
const auto queries_ll = geo::bench::queries_around(40.0, -74.0, 5.0, 1000);
96-
std::vector<Point> queries;
97-
queries.reserve(queries_ll.size());
98-
for (const auto& q : queries_ll) queries.push_back(to_boost_point(q));
99107

100108
for (auto _ : state) {
101-
for (const auto& q : queries) {
102-
benchmark::DoNotOptimize(bg::within(q, poly));
109+
for (const auto& q : queries_ll) {
110+
benchmark::DoNotOptimize(bg::within(to_boost_point(q), poly));
103111
}
104112
}
105-
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(queries.size()));
113+
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(queries_ll.size()));
106114
}
107115
BENCHMARK(BM_Boost_Contains)->Arg(10)->Arg(100)->Arg(1000);
108116

@@ -122,13 +130,13 @@ static void BM_Boost_Area(benchmark::State& state) {
122130
}
123131
BENCHMARK(BM_Boost_Area)->Arg(10)->Arg(100)->Arg(1000);
124132

125-
// --- path_length -----------------------------------------------------------
133+
// --- path_length: input is lat/lng; convert + sum inside the timed loop ---
126134

127135
static void BM_Boost_PathLength(benchmark::State& state) {
128136
const auto path_ll = geo::bench::random_points(static_cast<std::size_t>(state.range(0)));
129-
const LineString ls = to_boost_linestring(path_ll);
130137
bg::strategy::distance::haversine<double> haversine(kEarthRadius);
131138
for (auto _ : state) {
139+
LineString ls = to_boost_linestring(path_ll);
132140
benchmark::DoNotOptimize(bg::length(ls, haversine));
133141
}
134142
state.SetItemsProcessed(state.iterations() * state.range(0));

0 commit comments

Comments
 (0)