Skip to content

Speed up mjSpec editing: lazy signature and O(1) duplicate-name check - #3576

Open
ukanwat wants to merge 2 commits into
google-deepmind:mainfrom
ukanwat:perf/lazy-spec-signature
Open

ukanwat wants to merge 2 commits into
google-deepmind:mainfrom
ukanwat:perf/lazy-spec-signature

Conversation

@ukanwat

@ukanwat ukanwat commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Addresses the two dominant costs from #3397, as two commits.

  1. mjs_setName: the duplicate-name check copied and sorted every name of the type on each call, so naming N elements was O(N^2 log N). It now keeps a per-type count of names in use, built lazily and updated per rename, invalidated wherever names can change outside setName (attach, delete, copy, compile). Behaviour is unchanged, same error, same message.

  2. Signature: it was recomputed eagerly on every add/delete/attach by serialising the whole tree. It is now marked stale and recomputed on demand at compile, in CopyBack, and via a new mjs_getSignature(), which the Python .signature property and bind use. The eager value was already stale whenever a joint or sensor type was changed through the struct, and Compile recomputed it for that reason, so no existing guarantee changes. If you'd rather keep the field always valid, the alternative is a per-body subtree hash (O(depth) per add); happy to switch.

Adds test/benchmark/spec_benchmark_test.cc. On arm64 macOS: 8000 geoms 970 ms -> 79 ms, 8000 bodies 2360 ms -> 237 ms, 1000 nested bodies 650 ms -> 6 ms, 8000 named geoms 2700 ms -> 78 ms. Full ctest, Python suite and ASan clean.

I don't need git attribution if importing directly is easier.

mjs_setName called CheckRepeat, which copies every name of the element's
type into a vector, sorts it and scans for neighbours, so naming N
elements costs O(N^2 log N). Keep a per-type count of names in use
instead, built lazily and updated on each rename. Any structural change
(attach, delete, copy, compile) goes through ProcessLists, which
invalidates the counts since names can be rewritten there. Behaviour is
unchanged: a rename still fails if any repeated name exists within the
type, with the same error message, and the compile-time check is
untouched.

Adds spec_benchmark_test covering flat geoms, flat bodies, a nested body
chain and add-then-name geoms. On arm64 macOS naming 8000 geoms drops
from 2.7s to 1.0s, the same as adding them unnamed.
Every mjs_add*, delete and attach recomputed the spec signature eagerly,
which serializes the whole model tree and hashes it, so building a model
of N elements was O(N^2) (google-deepmind#3397). Mark the signature stale on structural
changes instead and recompute it on demand: at compile, in CopyBack, and
through the new mjs_getSignature(), which the Python .signature property
and bind now use. The eager value was already not authoritative between
edits, since changing joint or sensor types through the struct does not
touch it and Compile recomputes it for that reason, so no existing
guarantee is weakened.

Regenerates introspect/functions.py, the API docs and the WASM bindings
for the new function.

Spec benchmark on arm64 macOS: 8000 geoms 748 ms -> 79 ms, 8000 bodies
1784 ms -> 237 ms, 1000 nested bodies 458 ms -> 6 ms.
@ukanwat
ukanwat force-pushed the perf/lazy-spec-signature branch from 617504f to 1b34227 Compare September 21, 2026 00:56
copybara-service Bot pushed a commit that referenced this pull request Sep 21, 2026
GitHub Pull Request: #3576

Procedural model editing was quadratic in the number of elements (#3397): every
mjs_add* recomputed the spec signature, which serializes and hashes the whole
model tree, and mjs_setName rescanned every name of the element's type to look
for duplicates.

Behavioural changes:
- The signature is now cleared (set to 0) on structural edits and computed once
  at the end of mj_compile, so mjsElement.signature is 0 whenever the spec
  topology has been modified since it was last compiled.
- Duplicate names are tracked in a per-type std::unordered_set (names_), updated
  in place on mjs_setName and refreshed in ProcessLists. Failed mjs_setName
  calls now leave the element's previous name unchanged rather than overwriting
  it with the duplicate name.

Adds spec_benchmark_test. On arm64 macOS: 8000 geoms 748 ms -> 79 ms, 8000
bodies 1784 ms -> 237 ms, 1000 nested bodies 458 ms -> 6 ms, and naming 8000
geoms 2.7 s -> 1.0 s.

COPYBARA_INTEGRATE_REVIEW=#3576 from ukanwat:perf/lazy-spec-signature 617504f
PiperOrigin-RevId: 985370357
Change-Id: Ie6cd1dd412a651cfd2b6269779bbb7264052c507
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant