Repository navigation
perf: memoise curve caches in the curve value (outputs bit-identical) - #992
Merged
Merged
Conversation
make_curve_cache (SmCurve::CacheMakeOrValidate) keeps a memo of its result in the Curve value, as upstream keeps the cache in SmCacheMgr. Clones and evaluation copies share the memo, and every &mut edit of the geometry starts an empty one. The entry is keyed by the kernel state the build reads (tolerances and the context scalars). Every curve that is a pure value is memoised: B-spline, line, ellipse, hyperbola, parabola, Hermite and conic curves, and offset, projected and composite curves made only of these. A hit returns the cache a fresh build gives, copied into the caller's handle space. Kernel::memoize_curve_caches turns the memo off; the curve-memo-off feature makes that the default, so difftest runs in both positions. tests/curve_cache_memo.rs runs booleans with the memo on and off and compares every output bit for bit. The finding is in plan/findings/curve.curve_p5--curve-cache-memo.md. tangency-labs#948 loft, 20 sections: 1409 curve-cache builds (C++ 1393, main 16918). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
meros
force-pushed
the
perf/memo-curve-caches
branch
from
October 25, 2026 20:37
8647d5c to
1a2f705
Compare
meros
marked this pull request as ready for review
October 25, 2026 20:37
This was referenced Oct 25, 2026
bherbruck
approved these changes
Oct 25, 2026
bherbruck
left a comment
Collaborator
There was a problem hiding this comment.
Approved by the automated first-pass review: CI green, mergeable, no CI/build/dependency file changes, no red-flag patterns in added code (process/network/env/unsafe outside FFI/blobs/hidden unicode), 0 known-mismatch entries added. Logic is gated by CI and the difftest merge queue.
brett19
pushed a commit
to brett19/solidcore-fork
that referenced
this pull request
Oct 25, 2026
…AdvSurfaceIntersector (outputs bit-identical) SmAdvSurfaceIntersector::BranchIsGaussSeparate reads the polar box of a tree node and builds it lazily when it is missing (GetPolarBox). The port shares the surface caches, so it copied the whole surface cache (tree, patches, aux data) to build the box on the copy, and dropped the copy. SurfaceCache::polar_box_unstored builds the same box from a shared reference without storing it, as surface p8, p10, p11 and the silhouette code already do. It gives the box polar_box gives on a copy: the same chain from the nearest ancestor with a box, the same guesses, and the stored box on a failure. On the lofted solids of tangency-labs#948, on top of the curve-cache memo (tangency-labs#992) and tangency-labs#1006 and tangency-labs#993, this saves 1.8 % of the instructions at 20 sections and 2.0 % at 40, and 1.1 % of the CPU cycles at both (median of 3 runs). On main alone the change is below the noise. Results are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 26, 2026
Merged
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On the #948 loft at 20 sections, the port now builds 1,409 curve caches where the C++ builds 1,393 (main: 16,918). Per caller, the counts are the same as the C++ ones. The whole run takes 51 % fewer instructions at 20 sections and 42 % fewer at 40. No output changes, with the memo on or off. This is option C of #982, as refined in the issue's comments. Merging this PR accepts option C of #982. If you prefer another option, comment there and we will adapt this PR. Limit: the remaining gap to the C++ is the cost per build and other work, not the number of builds.
Cause
Upstream builds a curve's cache once and keeps it in
SmCacheMgruntil the curve is edited (SmCurve::Notify,SM_NO_PRE_EDIT). The port built a new cache for every request, unless one call chain kept it (#539, #753, #951). The Rust makes the same requests per caller as the C++ (22,661 at 20 sections, counted on #982). The C++ answers 94 % of them from its cache, and the port answered almost none.Change
make_curve_cache(upstreamSmCurve::CacheMakeOrValidate) keeps a memo of its result in theCurvevalue.Curvehas a private fieldmemo: CurveMemo, anOnceLock<Arc<Mutex<Option<entry>>>>.Cloneand the evaluation copies share it.kind_mutand the 5 editing methods that writeself.kindstart an empty one.Debugprints the memo as a constant.Kernel::tolerancesand the context's scalars (zone tolerance, model size estimate, size ratio,doing_boolean,trim_uv_curves_at_corner_drops). A lookup with another key is a miss. The owner is not an input, because the build works on its own ownerless copy.memo_pure): B-spline, line, ellipse, hyperbola, parabola, Hermite and conic curves, and offset, projected and composite curves made only of these. The walk over nested curves uses an explicit stack, bounded byMAX_GEOMETRY_NESTING. Iso curves, curves on surfaces and curves in volumes hold a surface or a volume with cache state of its own (Model lazy surface cache trees: GetDomainWithContinuity depends on whether the cache tree was built #568), so they always build.CurveCache<()>). A hit returns a copy in the caller's handle space (CurveCache::retag,Tree::retag_curve_tree). The build only stores the handle, so the copy is what a build with that handle gives. One entry serves the 32 handle types that the port uses for curve caches.Kernel::memoize_curve_cachesturns the memo off. The boolean's adapter kernels copy it. The new featurecurve-memo-offofsolidcoremakes off the default, so difftest and the tests run in both positions.Curve::clone_detached), so a memo never holds itself.curve.curve_p2,curve.curve_p5,curve.curve_cacheandgeom.treedescribe the memo. The finding is inplan/findings/curve.curve_p5--curve-cache-memo.md.No call site changes. The
*_cachedAPIs still work. Their first call now also hits the memo.Why the memo cannot change a result
The build takes
&Kernel, so it cannot draw from the RNG or record upstream UB. It writes nothing outside the new cache. Its result is a function of the curve's value and the key, and a hit returns that result. So the fill order, the clone that filled the memo and the thread cannot change a result.SM_DEFAULT_MAXCOUNT_CURVECACHE).unsafe.CurveisSendand notSyncon main, becauseCrvOnSurfand the offset surface holdCellandRefCell. It stays so, and the new test assertsSendforCurveandBrep.OnceLockandMutexcompile forwasm32-unknown-unknown, as theSurfacecache slot of port: face-bound trimmed surface cache and IntersectTSBound's cached branch #697 does.Test plan
Curve-cache builds per caller. I counted them with gdb, with a breakpoint on every
Tessellate(the method on #982), on the 20-section loft. C++ and main are the counts from #982.The two rows that differ from the C++ (
FindBoundaryStartPointsin the difference,EdgePointSolve) differ in their requests: the Rust makes requests there that the C++ does not make (#982). The memo is not the cause.Instructions and time. The #948 skins at 20 and 40 sections, run with
examples/toebox_loft.rs(local only, not in this PR). Release builds of main53fac4fe1and of this branch, pinned to the 4 performance cores, 3 runs each, interleaved with the C++ (c9c5979). The instruction counts are user space (cpu_core/instructions, whole run) and vary by less than 0.1 %. The machine load was 8 to 11 on 8 cores during the timed runs, so the times are higher than in #951 and #952 and their spread is wide.outer - inner(median, range)outer - inner(median, range)Bit identity.
tests/curve_cache_memo.rs. It runs 6 booleans: 4 of primitives, and 2 of lofted skins (skin - skin,skin ∩ box). Each runs with the memo off, then on, then a second time on the same kernel. The test compares the{:?}text of the result B-rep, its volume andupstream_ub_passed, so every value is compared bit for bit. It also asserts that every case gives a result. It passes with the default features and withcurve-memo-off.curve::curve::p5. A hit, a miss and a fresh build give the same cache, for a B-spline, a line segment and a Hermite curve, and for a clone in another handle space. An edit of one clone empties only its memo. Other tolerances do not match a stored key.Checks.
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo clippy -p solidcore --all-targets --features curve-memo-off -- -D warningsandcargo check -p solidcore --target wasm32-unknown-unknownpass.cargo test -p solidcorepasses: 2,221 tests, 0 failed.--unitin both positions, with--features cppand with--features cpp,solidcore/curve-memo-off. 31 units each, 0 failed,max_rel_err=0e0in each. The summary lines are the same in both positions:curve.curve_cache(642 cases),curve.curve_p1,curve.curve_p2,curve.curve_p5(3097),curve.curve_p6(3138),curve.curve_p8(3068),curve.composite_curve_p2(676);geom.tree(639),surface.surface_p8(2108),topology.face_p3(1781),topology.proto_topology_p3(3013);query.face_p2,query.loop,query.edge;intersect.curve_class_p5,_p7,_p9,intersect.point_class(2520),intersect.surface_intersector_p1,_p3,intersect.adv_surface_intersector(1931);heal.stitch_p1,boolean.api_brep,boolean.merge_p1to_p5,boolean.topology_intersector_p1to_p3.boolean.topology_intersector_p1keeps its 3 deferred cases. The rejected counts (boolean.api_brep8,boolean.merge_p43,intersect.curve_class_p92) are the same in both positions, and the first two are the same as in the perf: keep curve caches across repeated solves on the same curve (outputs bit-identical) #951 difftest run.Audit
clone_for_evaluationcall sites and everyCurve::clonenow share the memo. A curve made anew from the same geometry (Curve::new/Curve::from, 767 sites) starts with an empty memo. On this input those sites cost no extra builds, because the per-caller counts equal the C++ counts.O. There are 32 handle types for curve caches. All of them use the one handle-free entry. A hit copies the spans and the tree (CurveCache::retag), which was 0.6 % of the samples at 40 sections in a perf profile of the first version of this branch.make_curve_cacheuse options of their own (curve::curve::p5line 1631,surface::trim_srf_cache), so they are not memoised. They are inside the counts above.Not covered
&mut CurveCache(the lazy analytic curve). A shared read-only cache would save the copy.--greendifftest run (CI runs it).Related
Surfacecache slot, the precedent for a cache inside a geometry value.