Skip to content

perf: build a missing polar box without copying the surface cache in AdvSurfaceIntersector (outputs bit-identical) - #1033

Merged
bherbruck merged 1 commit into
tangency-labs:mainfrom
meros:perf/polar-box-no-copy
Oct 25, 2026
Merged

bherbruck merged 1 commit into
tangency-labs:mainfrom
meros:perf/polar-box-no-copy

Conversation

@meros

@meros meros commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

A surface/surface intersection copied the whole surface cache each time it needed a polar box that had not been built yet, then dropped the copy. This PR builds the box from the shared cache without copying it. On the lofted solids of #948, on top of #992, #1006 and #993, this saves 1.8 % of the instructions at 20 sections and 2.0 % at 40, and 1.1 % of the CPU cycles. On main alone the change is below the noise. No result changes.

Cause

SmAdvSurfaceIntersector::BranchIsGaussSeparate compares the polar boxes (normal cones) of two surface tree nodes and builds a missing box lazily (SmBezierAux2d::GetPolarBox). The port shares the surface caches between the solvers, so AdvSurfaceIntersector::polar_box (intersect/adv_surface_intersector.rs:656) did this:

let mut copy = cache.clone();
copy.polar_box(k, id)

It copied the whole cache (tree, patches, aux data) to build one box, and did not keep it. Our rule for copies is: clone only when both copies survive and are mutated independently. Here the copy is dropped at once and only one value is read, so this is the read-only case: compute from &SurfaceCache.

In a clone audit of the #948 run on main + #992 + #1006 + #993 (frame-pointer profile, perf cycles), this copy was 0.5 % of all samples at 20 sections and 0.3 % at 40.

Change

AdvSurfaceIntersector::polar_box calls SurfaceCache::polar_box_unstored(k, id). That function builds the box from a shared reference without storing it, as surface p8, p10 and p11 and the silhouette code already do for the same reason. It gives what polar_box gives on a copy:

  • a stored box when there is one;
  • otherwise the same chain from the nearest ancestor with a box (or the root) down to the node, with the same guesses;
  • the stored (uninitialised) box when the build fails;
  • unported callees still propagate.

The module header records the change.

This PR does not keep the built box, and upstream does. Keeping it on the shared cache needs the cache to be mutable or lazily filled through &self; that is part of the handle-agnostic cache design (issue draft: lazy parts in OnceLock).

Test plan

Input: the outer and inner skins of #948 at 20 and 40 sections, outer - inner, then ∩ box; examples/toebox_loft.rs (local only). Release builds, pinned to the 4 performance cores, 3 runs each, interleaved. The counts are user-space cpu_core events for the whole run.

On main b78b088a3 + #992 + #1006 + #993 (where the audit found the copy):

Sections without with this PR change
20, instructions 54.90 G 53.93 G -1.8 %
20, cycles (median of 3) 11.06 G 10.94 G -1.1 %
40, instructions 167.73 G 164.45 G -2.0 %
40, cycles (median of 3) 34.11 G 33.73 G -1.1 %

On main bff43b1bb alone, the difference is within the noise: instructions +0.05 % at 20 sections and -0.07 % at 40, cycles +0.2 % and -0.1 %. I did not find out why the instruction count at 20 sections is slightly higher on main.

  • Every non-timing output line (face, shell and edge counts, manifold flag, volume and bounding box at full precision) is identical to the build without the change in all 12 runs, both on main and on the stack.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo check -p solidcore --target wasm32-unknown-unknown, cargo test -p solidcore: pass.
  • difftest --unit, 0 failed and max_rel_err=0e0 in each: intersect.adv_surface_intersector (1931 cases), surface.surface_cache_p1 (872), intersect.surface_intersector_p5 (1725), topology.face_p3 (1781), boolean.api_brep, boolean.merge_p4.

Not run

  • The full --green difftest run (CI runs it).
  • Inputs other than these lofts.

Related

#948 (the input), #982, #992, #1006 and #993 (the stack where the copy shows).

…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>

@bherbruck bherbruck left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bherbruck
bherbruck added this pull request to the merge queue Oct 25, 2026
Merged via the queue into tangency-labs:main with commit 7c48775 Oct 25, 2026
13 checks passed
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.

2 participants