a5R can parallelise vectorised operations across threads with rayon, a Rust library for data parallelism. By default a5R uses a single thread, so there is no overhead; you opt in to parallelism when you need it. Work is split across elements, so a single polygon part, covering or centre cell gains nothing.
a5_threads() reports the thread count and
a5_threads(n) sets it, as options() and
par() do:
a5_info() also reports the count. To set it for a whole
script or batch job, set an R option or environment variable before
library(a5R); both are read when the package loads:
To use more threads for one block of code only, wrap it in
with(), as with mirai::daemons(). The previous
count is restored afterwards, even if the code fails:
set.seed(1)
cells <- a5_lonlat_to_cell(runif(1e4, -180, 180), runif(1e4, -80, 80), 10)
boundaries <- with(a5_threads(2), {
a5_cell_to_boundary(cells)
})
a5_threads()
#> [1] 1Setting a count returns the previous one, so
old <- a5_threads(4) and later
a5_threads(old) work too.
Threading applies to vectorised functions that process each element independently:
| Function | Per-element cost | Benefit |
|---|---|---|
a5_cell_to_boundary(),
a5_cell_to_boundary_xy() |
Heavy (boundary + WKB/WKT) | High |
a5_polygon_to_cells() |
Heavy (per-part fill, hole subtraction) | High |
a5_line_string_to_cells() |
Heavy (per-feature tracing) | High |
a5_grid_disk(), a5_grid_disk_vertex(),
a5_spherical_cap() |
Heavy (per-cell traversal) | High |
a5_cell_to_subcells() |
Heavy (per-cell search) | High |
a5_lonlat_to_cell(),
a5_cell_to_lonlat() |
Moderate (projection) | High |
a5_cell_to_supercell() |
Moderate (projection) | High |
a5_cell_distance() |
Moderate (2 projections + distance) | Medium |
a5_union(x, y), a5_intersect(),
a5_difference(), a5_contains(),
a5_overlaps() |
Varies with covering size | Medium |
a5_covering_to_supercells() |
Search within each covering | Medium |
a5_cell_to_children(), a5_uncompact() |
Light (per-element expansion) | Low |
a5_cell_to_parent(), a5_get_resolution(),
a5_migrate() |
Light (bit operations) | Low |
For the one-to-many functions (a5_grid_disk(),
a5_spherical_cap(), a5_cell_to_children(),
a5_uncompact()) the per-element work is parallelised;
assembling the result, especially the simplify = FALSE
list, is sequential, so the list form gains less than the flat form.
Scalar and bulk operations (a5_compact(),
a5_cell_area(), a5_cell_child(),
a5_cell_children_range()) are unaffected: they are already
fast or do not split by element.
Each parallel call pays a small fixed cost to hand work to the threads. For small vectors this can outweigh the benefit. As a rule of thumb:
Here is a comparison on about 695,000 cells:
cells <- a5_uncompact(a5_polygon_to_cells(wk::rct(-10, 50, 10, 60), resolution = 12))
length(cells)
#> [1] 694889
a5_threads(1)
system.time(a5_cell_to_boundary(cells))
#> user system elapsed
#> 0.952 0.005 0.956
a5_threads(8)
system.time(a5_cell_to_boundary(cells))
#> user system elapsed
#> 1.206 0.009 0.248user time (total CPU work across all threads) rises
while elapsed (wall-clock) time falls: that is the
parallelism at work.
a5R uses its own rayon thread pool, separate from R’s parallelism. Combining the two multiplies the thread count, so keep the product within your cores:
mirai,
future::multisession, PSOCK clusters) start at one a5R
thread, like any new session. Raise it inside a worker only if the
workers leave cores idle.parallel::mclapply(),
future::multicore) inherit the count but not the threads,
so a5R runs on one thread in each of them unless the worker calls
a5_threads(n) itself.Each a5_threads(n) call rebuilds the thread pool, which
takes well under a millisecond, so scoping a count with
with() around each heavy call costs nothing measurable.