Point-in-Polygon Acceleration Paths
Numba JIT: 1.06x the C extension on TimezoneFinder.timezone_at() over uniformly random points (slower).
pure Python: 40.4x the C extension on TimezoneFinder.timezone_at() over uniformly random points (slower).
Measured on Linux x86_64, Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz @ 2.4713 GHz, Python 3.13.16, across 2 environments on one machine.
The three point-in-polygon implementations, measured against each other rather than across commits. timezonefinder/utils.py binds one of them at import time, so which one a process runs is decided by its environment: the C extension wherever it loaded, the Numba-compiled function where it did not and numba is importable, and the plain Python function where neither is available. The rows below are therefore measured by rebinding the kernel rather than by installing a different environment. See Benchmarking Methodology.
Note
Nothing on this page is on the continuous-integration trend chart, which tracks the C extension alone - what a plain pip install timezonefinder runs. These are on-demand measurements, taken by make acceleration-paths.
How this is measured
Numba and pure Python are one source decorated or not, so no process holds both: what a process does hold is the C extension plus whichever of the two its environment produced. Each row below is therefore a paired comparison inside one process - the same random draw handed to both candidates, the order alternating round by round, and two estimators reported so that a difference neither can demonstrate reads as unresolved rather than as a number (benchmarks/candidate_comparison.py).
That also fixes what this page will not do: divide one environment’s column into the other’s. Two runs in two processes can alternate nothing and share no draw, so a Numba-against-pure-Python ratio taken that way would rest on the single estimator this repository has already been misled by (Benchmarking Methodology). The two measured pairs are published; the third ratio is not derived from them.
The kernel a lookup reaches
PolygonArray.pip over the packed payload, one pass over 2,500 committed (point, ring) pairs per round, split by polygon size so the largest rings are not hidden behind an average over a mostly-small population.
Numba JIT against the C extension
Workload |
C extension (clang) |
Numba JIT |
Relative to clang |
Rounds won |
Verdict |
|---|---|---|---|---|---|
small polygons |
1.70ms |
2.69ms |
1.58x |
0 of 61 |
slower |
medium polygons |
1.87ms |
2.90ms |
1.55x |
0 of 61 |
slower |
large polygons |
3.70ms |
4.97ms |
1.34x |
0 of 61 |
slower |
pure Python against the C extension
Workload |
C extension (clang) |
pure Python |
Relative to clang |
Rounds won |
Verdict |
|---|---|---|---|---|---|
small polygons |
1.68ms |
5.62ms |
3.35x |
0 of 15 |
slower |
medium polygons |
1.92ms |
153ms |
79.6x |
0 of 15 |
slower |
large polygons |
3.76ms |
432ms |
115x |
0 of 15 |
slower |
What a caller actually pays
TimezoneFinder.timezone_at() in memory, over the same committed point fixtures the other pages use. The kernel ratio above bounds these and does not predict them: the shortcut index answers a unique-shortcut query outright, so no point-in-polygon test runs at all and the two paths are measuring the same code.
Numba JIT against the C extension
Workload |
C extension (clang) |
Numba JIT |
Relative to clang |
Rounds won |
Verdict |
|---|---|---|---|---|---|
random points |
2.88ms |
3.05ms |
1.06x |
6 of 61 |
slower |
unique-shortcut points |
2.47ms |
2.47ms |
1.00x |
22 of 61 |
unresolved |
ambiguous-shortcut points |
13.8ms |
15.6ms |
1.13x |
0 of 61 |
slower |
pure Python against the C extension
Workload |
C extension (clang) |
pure Python |
Relative to clang |
Rounds won |
Verdict |
|---|---|---|---|---|---|
random points |
2.93ms |
118ms |
40.4x |
0 of 15 |
slower |
unique-shortcut points |
2.49ms |
2.49ms |
1.00x |
8 of 15 |
no difference |
ambiguous-shortcut points |
14.0ms |
3.51s |
250x |
0 of 15 |
slower |
What installing Numba costs before any query
Speed is only one consequence of installing the numba extra, and it is the only one the tables above can see: a paired comparison runs both candidates in one process, which has already imported whatever its environment holds. The rows below are therefore not paired - each is a fresh process that imports the package, constructs a default TimezoneFinder() and answers one query, measured by scripts/_startup_probe.py and reported as the median of its repetitions. Both environments hold the C extension and both bind it, so the two processes run the same kernel and differ in exactly one thing - whether Numba is installed beside it. The last column is therefore what the extra costs an installation that already has a working extension, and since the package imports Numba only where the JIT kernel is what answers queries, the honest answer is expected to be nothing: a row that shows otherwise means something reached into utils_numba on the way to a lookup again.
Resident memory
Step |
Plain install |
With Numba |
Cost of the extra |
|---|---|---|---|
added by |
4.67 MiB |
4.64 MiB |
-24.0 KiB |
added by |
19.8 MiB |
19.9 MiB |
+56.0 KiB |
added by the first query |
144 KiB |
132 KiB |
-12.0 KiB |
the whole process, once the first answer is out |
37.7 MiB |
37.6 MiB |
-64.0 KiB |
Every row but the last is what that step added; the last is the whole process, which is the figure a container memory limit is set against. The difference is the Numba and llvmlite import plus the code it compiles, and it is paid per process - a worker pool pays it once per worker, and it does not shrink for TimezoneFinderL or for the memory-mapped default mode, because it is not boundary data (TimezoneFinder Memory Footprint measures that part).
Time to the first answer
Step |
Plain install |
With Numba |
Cost of the extra |
|---|---|---|---|
|
29.2ms |
29.5ms |
+282µs |
|
112ms |
111ms |
-965µs |
the first query |
68.3µs |
61.2µs |
-7.04µs |
the whole way to the first answer |
142ms |
141ms |
-690µs |
Where there is a compilation at all it lands in the construction row rather than in the query one: timezonefinder/utils_numba.py declares eager signatures, so the kernels are compiled when that module is first imported, which constructing a finder triggers on an installation without the C extension. Numba caches that output on disk beside the installed package, so the figures here are the steady state - the median over repetitions discards the one process that compiled - and the first process in a fresh environment pays more. Where the installed package directory is read-only or thrown away between runs, every process pays it. Later queries are what the tables above measure; this is what a process pays before the first of them, and a short-lived process or a cold-started function pays it per invocation.
How comparable the two runs are
Both runs measure the C extension, so its two timings are the same quantity measured twice and their spread says how much of the gap between the tables above is the environment rather than the point-in-polygon path. Published rather than used as a gate, because nothing here divides one run into the other.
The kernel a lookup reaches
Workload |
clang, in the Numba JIT run |
clang, in the pure Python run |
Spread |
Within 3 % |
|---|---|---|---|---|
small |
1.70ms |
1.68ms |
1.5 % |
yes |
medium |
1.87ms |
1.92ms |
2.8 % |
yes |
large |
3.70ms |
3.76ms |
1.6 % |
yes |
What a caller actually pays
Workload |
clang, in the Numba JIT run |
clang, in the pure Python run |
Spread |
Within 3 % |
|---|---|---|---|---|
random |
2.88ms |
2.93ms |
1.8 % |
yes |
unique shortcut |
2.47ms |
2.49ms |
0.6 % |
yes |
ambiguous shortcut |
13.8ms |
14.0ms |
1.4 % |
yes |
Two things move these rows. The first is ordinary run-to-run variation: the C kernel is the same compiled code in both runs, so wherever its two timings differ that is the floor for comparing anything across the two processes - and it is wider than the 3 % a paired comparison inside one process resolves, which is the whole reason this page does not divide one run into the other.
The second used to be specific to the Numba run and is not any more: timezonefinder/utils.py imports utils_numba only where the C extension is missing, so a process that merely has Numba installed no longer imports it - same kernel, same imports, same resident set as a plain install, which is what the section below now measures. What still differs is everything that reaches into utils_numba on purpose: the kernel rows on this page, and benchmarks/test_inside_polygon.py’s *_python rows, which are JIT-compiled wherever Numba is installed.