Sort-Tile-Recursive packing
BulkLoad sorts entry centers by axis, tiles them into capacity-bounded nodes, then packs parent levels. Use it to build an empty tree from a batch.
Highly optimised spatial indexing
Tedd.RTree indexes axis-aligned 2D rectangles and 3D boxes for intersection queries. Choose int, long, float, or double coordinates, with individual updates, packed bulk loading, and two concurrent update models.
Fastest in the published five-package comparison: lowest measured mean for both bulk construction and aggregate intersection queries in all four 1,000- and 10,000-entry uniform and clustered fixtures. This ranking applies to the tested 2D double workload.
In the measured 10,000-entry uniform fixture, bulk build took 2.15 ms versus 5.81 ms for RBush and 4.96 ms for NTS STRtree. A batch of 64 queries took 253.94 µs versus 1148.05 µs and 1777.96 µs. See all five packages and benchmark conditions.
Incremental insertionLeast enlargement selection and quadratic node splits
Bulk loadingSort-Tile-Recursive packing on an empty tree
Inclusive searchBoundary contact is included in 2D and 3D
Result ownershipReturn a list or append to a caller-owned list
Under the hood
The index prunes nonintersecting branches before testing entries. Packed construction and incremental updates use different algorithms for their respective workloads.
BulkLoad sorts entry centers by axis, tiles them into capacity-bounded nodes, then packs parent levels. Use it to build an empty tree from a batch.
Individual inserts descend through the child whose bounds need the smallest enlargement, breaking ties by current area. Overflow uses quadratic seed selection and distribution.
Search skips nodes whose bounds do not intersect the query. Reuse a caller-owned result list to avoid a fresh result collection; SearchBatch processes several queries with caller-owned outputs.
The benchmark ranking measures the original 2D double tree. Coordinate-generic 2D and 3D variants, concurrent updates, and individual query widths have separate performance characteristics. Review the measured fixtures and limitations.
Choose an index
The difference is when a search can see a change. Each update model supports coordinate-generic 2D and 3D bounds.
| Type | How to change it | What a search sees | Use when |
|---|---|---|---|
RTree<T>Plain tree | Insert, remove, or move entries directly. Bulk load an empty tree. | Current contents. Do not change the tree while any search is running. | One caller controls changes, or your application provides its own synchronisation. |
ConcurrentRTree<T>Immediate updates | Add, move, or remove one value at a time. Readers and writers use a reader/writer lock. | Each completed change is visible to later searches. A search and a change may wait for each other. | Separate threads search and move individual entries, and each move must become visible promptly. |
SnapshotRTree<T>Published batches | Build a complete replacement with ReplaceAll; publish it in one step. | One complete version. A search already running may finish on the previous version; readers do not wait for the build. | Most entries move together and searches can use the latest published batch. |
RTree<T>Call Insert, Remove(bounds, item), or Update(oldBounds, item, newBounds). Duplicate values are allowed. Simultaneous searches are safe while the tree remains unchanged, with a separate result list per search.
ConcurrentRTree<T>Call Add, Move(item, newBounds), or Remove(item); an empty index also accepts an initial BulkLoad. Values are unique, stable keys. A write holds the lock until it completes; later searches see the new position. Dispose the index after callers stop using it.
SnapshotRTree<T>Call ReplaceAll(entries) with every current position. The next tree is built separately, then published. Searches remain lock-free and never traverse a partly built tree. Old versions may remain while searches finish; the GC reclaims them later.
Coordinate variants. Use RTree2D<TCoordinate, T> with Rectangle2D<TCoordinate>, or RTree3D<TCoordinate, T> with Box<TCoordinate>. The snapshot and concurrent variants follow the same 2D/3D names. A voxel at (x, y, z) is a point query with new Box<int>(x, y, z, x, y, z). The original unsuffixed types remain available for 2D double coordinates. Search(bounds) returns a list; Search(bounds, results) appends to a caller-owned list. Each simultaneous search needs its own list. Result order is unspecified.
Batch searches. All tree variants accept SearchBatch(queries, results, counts). Supply one non-null result list per query and at least one count slot per query. Matches append to the lists; counts report the number appended per query, and the return value is the total as a long. A concurrent batch holds one read lock until all queries complete. A snapshot batch uses one published version throughout. Batches run sequentially; simultaneous batches need separate output storage.
C# examples
These examples use the original 2D double API. The first returns a new result list; the second appends to a list owned by the caller.
using Tedd.RTree;
var tree = new RTree<string>(maxEntries: 16);
var oldBounds = new Rectangle(0, 0, 10, 10);
tree.Insert(oldBounds, "A");
tree.Insert(new Rectangle(20, 20, 30, 30), "B");
tree.Update(oldBounds, "A", new Rectangle(5, 5, 15, 15));
List<string> matches = tree.Search(
new Rectangle(8, 8, 12, 12));
// matches contains "A"
using Tedd.RTree;
var entries = new[]
{
new SpatialEntry<int>(new Rectangle(0, 0, 4, 4), 1),
new SpatialEntry<int>(new Rectangle(5, 5, 9, 9), 2)
};
var tree = new RTree<int>();
tree.BulkLoad(entries);
var results = new List<int>();
int added = tree.Search(new Rectangle(3, 3, 6, 6), results);
// added == 2
Run dotnet add package Tedd.RTree in a .NET 10 project.
Moving entries
Use SnapshotRTree<T> when searches may use the latest published positions while the next batch is built. Use ConcurrentRTree<T> when each completed move must be visible to the next search. At 10,000 entries, rebuilding took 1.75 ms; moving all entries individually took 14.77 ms in the local update benchmark.
using Tedd.RTree;
var index = new SnapshotRTree<int>();
index.ReplaceAll(new[]
{
new SpatialEntry<int>(new Rectangle(0, 0, 10, 10), 42)
});
List<int> matches = index.Search(new Rectangle(5, 5, 6, 6));
using Tedd.RTree;
using var index = new ConcurrentRTree<int>();
index.Add(new Rectangle(0, 0, 10, 10), 42);
index.Move(42, new Rectangle(20, 20, 30, 30));
List<int> matches = index.Search(new Rectangle(20, 20, 21, 21));
Five validated packages · .NET 10
Build and intersection-query throughput on identical rectangles. Teal identifies Tedd.RTree; grey identifies competing packages. Higher bars mean more work per second. Each query creates its result collection.
Mean ± sample standard deviation · managed allocation per operation
| Entries | Distribution | Operation | Tedd.RTree | RBush | NTS STRtree | RTree | Enyim RTree |
|---|---|---|---|---|---|---|---|
| 1,000 | Uniform | Build | 0.123 ± 0.004 ms133.72 KiB allocated | 0.230 ± 0.001 ms156.99 KiB allocated | 0.234 ± 0.049 ms129.86 KiB allocated | 0.883 ± 0.025 ms314.43 KiB allocated | 0.401 ± 0.011 ms68.11 KiB allocated |
| 1,000 | Clustered | Build | 0.114 ± 0.003 ms133.72 KiB allocated | 0.325 ± 0.005 ms156.99 KiB allocated | 0.364 ± 0.005 ms129.86 KiB allocated | 0.618 ± 0.019 ms313.09 KiB allocated | 0.377 ± 0.004 ms68.11 KiB allocated |
| 10,000 | Uniform | Build | 2.153 ± 0.081 ms1,308.81 KiB allocated | 5.811 ± 0.126 ms2,165.52 KiB allocated | 4.959 ± 0.291 ms1,399.61 KiB allocated | 11.937 ± 0.627 ms3,023.33 KiB allocated | 6.501 ± 0.376 ms862.78 KiB allocated |
| 10,000 | Clustered | Build | 2.226 ± 0.046 ms1,308.81 KiB allocated | 5.803 ± 0.309 ms2,165.52 KiB allocated | 4.390 ± 0.011 ms1,399.61 KiB allocated | 11.539 ± 0.247 ms3,018.80 KiB allocated | 5.657 ± 0.256 ms862.78 KiB allocated |
| 1,000 | Uniform | 64 queries | 29.290 ± 3.099 µs50.67 KiB allocated | 81.347 ± 1.327 µs108.04 KiB allocated | 68.640 ± 4.009 µs50.67 KiB allocated | 120.743 ± 2.025 µs56.67 KiB allocated | 793.258 ± 61.230 µs116.62 KiB allocated |
| 1,000 | Clustered | 64 queries | 25.580 ± 1.622 µs55.02 KiB allocated | 75.435 ± 1.331 µs117.44 KiB allocated | 84.799 ± 1.431 µs55.02 KiB allocated | 114.663 ± 3.932 µs61.02 KiB allocated | 1076.541 ± 18.530 µs129.25 KiB allocated |
| 10,000 | Uniform | 64 queries | 253.936 ± 3.108 µs702.32 KiB allocated | 1148.046 ± 34.031 µs1,509.62 KiB allocated | 1777.963 ± 19.346 µs702.32 KiB allocated | 2001.055 ± 104.507 µs708.32 KiB allocated | 11443.148 ± 210.574 µs1,435.88 KiB allocated |
| 10,000 | Clustered | 64 queries | 197.635 ± 5.934 µs634.80 KiB allocated | 959.356 ± 21.793 µs1,377.21 KiB allocated | 1352.807 ± 84.770 µs634.80 KiB allocated | 1731.322 ± 49.235 µs640.80 KiB allocated | 11627.599 ± 604.620 µs1,297.76 KiB allocated |
30 September 2026 · AMD Ryzen 9 5950X · Windows 11 · .NET 10.0.12 · BenchmarkDotNet 0.15.8 · one launch, three warm-ups and three measured iterations. Fixed seed 73211; integer-valued bounds; 64 windows with widths 0, 20, 200 and 1,000 units. Input geometry and package adapters are prepared outside timing. Every query's result IDs are checked against brute force before measurement. These results describe this fixture; small differences require further measurement.
Tedd.RTree, RBush 4.0.0, Enyim.Collections.RTree 1.0.5 and NetTopologySuite 2.6.0 use bulk construction. RTree 1.1.0 uses incremental insertion, with the 2D data embedded at z = 0 in its 3D float index. Node capacity is 16. STRtree does not accept inserts after build. Enyim targets .NET Framework and its published binary has optimizations disabled; it passed the .NET 10 checks and is measured as distributed. The concurrent wrappers have separate measurements.
Its first uniform 1,000-entry validation returned 39 matches where brute force found 40, omitting ID 942. The package is included in the reproducible validation probe, but incorrect result sets are not ranked as faster queries.
Each index holds 10,000 unit bounds at the same integer-valued positions. The generic variants use int, long, float, or double coordinates. Times are means ± sample standard deviation; lower is better within the same dimension and query kind.
64 windows per operation · reused result list · zero measured allocation
| Dimension | Coordinates | 64 point queries | 64 region queries |
|---|---|---|---|
| 2D | int | 7.85 ± 0.25 µs | 40.26 ± 2.61 µs |
| 2D | long | 7.64 ± 0.57 µs | 49.07 ± 14.98 µs |
| 2D | float | 7.69 ± 0.77 µs | 38.27 ± 1.32 µs |
| 2D | Generic double | 6.65 ± 0.44 µs | 52.48 ± 3.19 µs |
| 3D | int | 7.82 ± 0.36 µs | 20.90 ± 1.67 µs |
| 3D | long | 9.21 ± 1.42 µs | 24.32 ± 3.29 µs |
| 3D | float | 11.43 ± 0.32 µs | 28.98 ± 5.12 µs |
| 3D | double | 12.57 ± 0.31 µs | 22.99 ± 1.12 µs |
10,000 prepared entries · one packed tree per operation
| Dimension | Coordinates | Build time | Allocated |
|---|---|---|---|
| 2D | int | 2.37 ± 0.07 ms | 1,120 KB |
| 2D | long | 2.38 ± 0.04 ms | 1,475 KB |
| 2D | float | 1.97 ± 0.10 ms | 953 KB |
| 2D | Generic double | 2.06 ± 0.04 ms | 1,309 KB |
| 3D | int | 3.47 ± 0.09 ms | 1,469 KB |
| 3D | long | 3.68 ± 0.05 ms | 2,002 KB |
| 3D | float | 2.70 ± 0.07 ms | 1,219 KB |
| 3D | double | 2.82 ± 0.07 ms | 1,752 KB |
AMD Ryzen 9 5950X · Windows 11 · .NET 10.0.12 · BenchmarkDotNet 0.15.8. Query results use ten measured iterations; builds use five. Geometry creation and tree construction are outside query timing. Integer coordinates remain exact, including long positions above 253. The results describe this fixture; close differences and high-variance cases require measurement in the target application.
API boundaries
Geometry. Coordinates must be finite and ordered. Rectangles are 2D; boxes are 3D. Both are axis-aligned, and search includes boundary contact. Represent a single voxel position with equal minimum and maximum coordinates.
Updates. BulkLoad requires an empty tree. Individual inserts, removals, and moves can follow. When most entries move together, rebuilding a packed tree is faster than moving each entry in place on the measured 10,000-entry fixture.
Concurrency. The API comparison describes the three visibility contracts. SnapshotRTree publishes complete batches for lock-free searches. ConcurrentRTree makes individual moves visible under a reader/writer lock. Measured costs.
Licence and package. The repository and NuGet package use LGPL-2.1. The package targets .NET 10.