Skip to content

Speed up BaseStore ID lookups and removals - #1341

Open
j-rivero wants to merge 2 commits into
mainfrom
perf/index-base-store-ids
Open

j-rivero wants to merge 2 commits into
mainfrom
perf/index-base-store-ids

Conversation

@j-rivero

Copy link
Copy Markdown
Contributor

🦟 Bug fix

Summary

Doing some experiments with agents locally, one of them found a performance problem in the way we are handling BaseStore IDs

GetById, ContainsId, RemoveById and DestroyById resolved IDs with a linear scan over the store. The ogre2 thermal, GPU rays, segmentation and bounding box material switchers call Scene::VisualById for every Ogre item on every rendered pass (GPU rays once per cubemap face), so the per-frame cost grows with the square of the scene size.

Objects created through BaseScene get IDs from a decrementing counter and stores only append or erase, so the store vector is normally ordered by (first ID - ID). PR changes: ConstIterById now bisects on that key, which stays ordered across unsigned wrap-around, and falls back to the previous linear scan when the bisection misses. Stores holding caller-chosen IDs in any other order return the same results as before.

RemoveImpl walked the whole name map on every removal, including when removing the last element. Erase by the object's name and skip the re-indexing at the tail, which makes DestroyAll linear.

Numbers

Measured with the real header and a heap-scattered mock object at -O2, with IDs assigned the way BaseScene::CreateObjectId does:

case before after
one GetById per object, N=4096 5.6 ms 0.29 ms
one GetById per object, N=16384 152 ms 1.8 ms
DestroyAll, N=16384 476 ms 2.0 ms

Stores filled with increasing caller-chosen IDs keep the previous lookup cost.

Tests

The PR includes a test that verifies how the IDs are handled by the BaseScene so we have some warning if we are breaking one of the considered use cases.

Backport Policy

  • This is safe to backport to the following versions:
    • Jetty
    • Ionic
    • Harmonic
    • Fortress
  • This should not be backported
  • I am not sure
  • Other (fill in yourself)

Checklist

  • Signed all commits for DCO
  • Added a screen capture or video to the PR description that demonstrates the fix (as needed)
  • Added tests
  • Updated documentation (as needed)
  • Updated migration guide (as needed)
  • Consider updating Python bindings (if the library has them)
  • codecheck passed (See contributing)
  • All tests passed (See test coverage)
  • Updated Bazel files (if adding new files). Created an issue otherwise.
  • While waiting for a review on your PR, please help review another open pull request to support the maintainers
  • Was GenAI used to generate this PR? If so, make sure to add "Assisted-by" to your commits. (See this policy for more info.)

Assisted-by: Opencode (Union Alpha Free), Claude Opus 5

Note to maintainers: Remember to use Squash-Merge and edit the commit message to match the pull request summary while retaining Signed-off-by and Generated-by messages.

Backports: If this is a backport, please use Rebase and Merge instead.

BaseStore had no direct test coverage; its lookups were only exercised
through the engine-backed common tests. Add a unit test with a minimal
object type that checks every object stays reachable by index, ID, name
and pointer across additions, duplicate rejection, each removal accessor,
DestroyAll and RemoveAll.

IDs are fed in decreasing order (as BaseScene::CreateObjectId assigns
them), decreasing with wrap-around past zero, increasing, and unordered.

Assisted-by: Claude Opus 5
Signed-off-by: Jose Luis Rivero <jrivero@honurobotics.com>
GetById, ContainsId, RemoveById and DestroyById resolved IDs with a
linear scan over the store. The ogre2 thermal, GPU rays, segmentation
and bounding box material switchers call Scene::VisualById for every
Ogre item on every rendered pass (GPU rays once per cubemap face), so
the per-frame cost grows with the square of the scene size.

Objects created through BaseScene get IDs from a decrementing counter
and stores only append or erase, so the store vector is normally ordered
by (first ID - ID). ConstIterById now bisects on that key, which stays
ordered across unsigned wrap-around, and falls back to the previous
linear scan when the bisection misses. Stores holding caller-chosen IDs
in any other order return the same results as before.

RemoveImpl walked the whole name map on every removal, including when
removing the last element. Erase by the object's name and skip the
re-indexing at the tail, which makes DestroyAll linear.

No data members or signatures change, so the change is ABI compatible
and can be backported.

Measured with the real header and a heap-scattered mock object at -O2,
with IDs assigned the way BaseScene::CreateObjectId does:

  case                                 before     after
  one GetById per object, N=4096       5.6 ms     0.29 ms
  one GetById per object, N=16384      152 ms     1.8 ms
  DestroyAll, N=16384                  476 ms     2.0 ms

Stores filled with increasing caller-chosen IDs keep the previous lookup
cost.

Assisted-by: Opencode (Union Alpha Free), Claude Opus 5
Signed-off-by: Jose Luis Rivero <jrivero@honurobotics.com>
@j-rivero

Copy link
Copy Markdown
Contributor Author

Asked the agents to turn this into a real experiment that demonstrate how the gain is tangible, the results makes sense to me like nothing is gained in small worlds but there are important gains in large worlds. Take it with a grain of salt (mostly vibe-coded experiment) or double verify the numbers under your own experiment:

The gain depends on scene size. In gz-sim's stock tunnel world there is no measurable change. With the tunnel's models repeated 16 times (2,226 models, 9,534 visuals), the lidar scans 2.1× faster, and 2.5× faster when the robot's four RGBD cameras also run.

Setup: the installed gz-sim 11 nightly, run headless with ogre2. Both builds are gz-rendering 9cd640c (the commit the nightly was built from) and differ only in BaseStorage.hh from aad8d1e. Every run confirmed from the process's memory map that it loaded the libraries under test. The robot is the Explorer R2 from Fuel: a 360° lidar at 15 Hz and four RGBD cameras at 30 Hz. The ×4 and ×16 worlds repeat every model 3 km apart, so the robot sees the same tunnel while the scene grows. Physics ran as fast as possible, and I alternated which build ran first. Results are medians of 5 runs (3 for lidar + cameras).

│ World │ Visuals │ Sensors │ Before (ms per scan) │ After (ms per scan) │ Gain │
│ Stock tunnel │ 609 │ lidar │ 30.0 │ 30.3 │ none │
│ Stock tunnel │ 609 │ lidar + 4 RGBD │ 60.0 │ 56.1 │ 1.07× │
│ Tunnel ×4 │ 2,394 │ lidar │ 86.0 │ 85.3 │ none │
│ Tunnel ×16 │ 9,534 │ lidar │ 1,131 │ 538 │ 2.10× │
│ Tunnel ×16 │ 9,534 │ lidar + 4 RGBD │ 1,442 │ 567 │ 2.54× │

In practice, one simulated minute in the ×16 world with all sensors went from 21.8 to 8.6 minutes of wall time. Load time stays at 74 s with either build.

Why ×4 gains nothing but ×16 gains 2×: a scan takes as long as the slower of two threads, because the simulation thread waits for the previous render before handing over new poses.

  • Where the time goes: I built a timed copy of each variant to measure lookup cost. Almost all lookups come from the lidar's material switcher at Ogre2GpuRays.cc:325, which looks up every object in the scene on each of the 4 directions the lidar renders, whether in range or not. The cameras make no ID lookups.
  • At ×4: lookups took 53 ms of each 86 ms scan, and after the fix 0.7 ms. But the simulation thread was already at 98.5% of a core and setting the pace. The render thread's CPU fell from 89% to 26% of a core, but scans didn't get faster.
  • At ×16: lookups took 1,084 ms per scan (92% of wall time), and after the fix 4.6 ms. The render thread had been saturated; after the fix the simulation thread (99.4%) sets the pace.
  • Lookup work: a lookup went from scanning about 9,200 objects to about 13 bisection steps. No lookup fell back to the linear scan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Inbox

Development

Successfully merging this pull request may close these issues.

2 participants