Skip to content

perf: first-derivative surface evaluation without per-call heap or array moves (outputs bit-identical) - #1087

Merged
bherbruck merged 1 commit into
tangency-labs:mainfrom
meros:perf/srf-derivs-into
Oct 26, 2026
Merged

bherbruck merged 1 commit into
tangency-labs:mainfrom
meros:perf/srf-derivs-into

Conversation

@meros

@meros meros commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

In short: the volume computation evaluates a surface point and its two tangents at every integration step, and each evaluation allocated memory and copied scratch tables. It now writes into fixed buffers. Results do not change. On the loft case of #948 the volume step takes about 40 % fewer profile samples, and the whole run uses 2 to 4 % fewer CPU cycles.

What is wrong

On the #948 loft (20 sections), the volume step is 384 profile samples in Rust against 142 in the C++. 273 of the Rust samples are surface evaluation under OdeIsoCurveLength::evaluate -> Surface::evaluate_simple -> srf_basic::Surface::derivs. Per call that path:

  • allocates a BasisDerivs Vec and a Vec<Vec<Vec3>> result;
  • moves two Result<(isize, [[f64; 8]; 5])> tables out of univariate_derivs (a memmove each);
  • zeroes 8-wide scratch tables for surfaces of degree 3 or less.

The C++ (N_SrfDerivs) uses stack arrays and the caller's output array.

Copy or allocation Rule case Replaced by
univariate_derivs result returned by value output buffer fills the caller's table
BasisDerivs Vec and Vec<Vec<Vec3>> per 2x2 evaluation output buffer first_derivs_into writes the caller's [[Vec3; 2]; 2]

Change

  • univariate_derivs writes into the caller's zeroed table.
  • derivative_matrix takes the basis derivative as a closure and writes into the caller's matrix. Surface::derivs keeps its Vec result for every other caller.
  • The new Surface::first_derivs_into (orders 1, 1) writes a [[Vec3; 2]; 2]. For a non-rational surface it reads Du[k][i] * Dv[l][j] straight from the two univariate tables: the same product basis_derivs stores, so the same bits. Rational surfaces still go through basis_derivs.
  • BSplineSurface::evaluate_simple_first_into and Surface::evaluate_simple_first_into carry the 2x2 block up. Kinds that are not B-spline based fall back to evaluate_simple with the same copy semantics as before. The thin-face branch of OdeIsoCurveLength::evaluate uses it.
  • The scratch tables are 4 wide (TINY_NP) when both degrees are below 4, next to the existing 8-wide case. Every index used is at most the degree, so values and errors stay the same (the argument the 8-wide case already makes).

Test plan

toebox_loft <sections> 1 full (the #948 driver, a local example, not in this PR), release build, main b206d3f5a against this branch, 3 interleaved runs each, taskset -c 0-3, perf stat -e instructions:u,cycles:u,task-clock, medians (load 1 to 3; the three runs of each side are within 1 %):

Sections Metric main branch Change
20 cycles (user) 10.84 G 10.42 G -3.8 %
20 instructions (user) 54.87 G 52.53 G -4.3 %
40 cycles (user) 33.53 G 32.90 G -1.9 %
40 instructions (user) 167.59 G 163.53 G -2.4 %

Volume-phase profile samples (frame-pointer builds of the same two trees, perf record -F 1000, 20 sections, 2 runs each):

main branch
samples under brep_volume 295, 241 158, 159
of which under OdeIsoCurveLength::evaluate 216, 182 105, 104
of which memmove/memset/malloc/free leaves 65, 53 4, 11

At this speed the per-step volume took times (whole milliseconds) vary more between runs than the change, so the samples are the volume-phase measure. All non-timing output (counts, volumes and bounding boxes to 17 digits) is identical across all runs of main and the branch, for both inputs.

Checks

  • cargo fmt --all --check: pass
  • cargo clippy --workspace --all-targets -- -D warnings: pass
  • cargo test -p solidcore: 2275 passed, 0 failed. New test: first_derivs_into equals derivs bit for bit, errors included, over 7 surfaces (rational and not, degrees 1 to 8, so all three scratch widths), parameters inside and outside the domain, both sides and both upper-half modes.
  • cargo check -p solidcore --target wasm32-unknown-unknown: pass
  • difftest --unit, all failed=0 deferred=0 max_rel_err=0e0:
PASS nlib.srf_basic cases=3220 passed=3220 failed=0 deferred=0 max_rel_err=0e0
PASS nlib.srf_nurb cases=2711 passed=2711 failed=0 deferred=0 max_rel_err=0e0
PASS nurbs.b_spline_surface_p1 cases=2120 passed=2120 failed=0 deferred=0 max_rel_err=0e0
PASS nurbs.b_spline_surface_n_lib_p1 cases=699 passed=699 failed=0 deferred=0 max_rel_err=0e0
PASS nurbs.b_spline_surface_n_lib_p2 cases=774 passed=774 failed=0 deferred=0 max_rel_err=0e0
PASS nurbs.nurbs_srf cases=3092 passed=3092 failed=0 deferred=0 max_rel_err=0e0
PASS surface.surface_p1 cases=1000 passed=1000 failed=0 deferred=0 max_rel_err=0e0
PASS surface.surface_cache_p2 cases=905 passed=905 failed=0 deferred=0 max_rel_err=0e0
PASS surface.surface_point_solve_p1 cases=1035 passed=1035 failed=0 deferred=0 max_rel_err=0e0
PASS surface.surface_point_solve_p2 cases=1353 passed=1353 failed=0 deferred=0 max_rel_err=0e0
PASS surface.surface_point_solve_p3 cases=1365 passed=1365 failed=0 deferred=0 max_rel_err=0e0
PASS intersect.surface_drop_lib_p3 cases=1529 passed=1529 failed=0 deferred=0 max_rel_err=0e0
PASS query.face_p2 cases=1401 passed=1401 failed=0 deferred=0 max_rel_err=0e0
PASS topology.face_p2 cases=2692 passed=2692 failed=0 deferred=0 max_rel_err=0e0
PASS mass.integrator cases=1528 passed=1528 failed=0 deferred=0 max_rel_err=0e0

Not run

  • difftest --green (the CI gate) did not run locally; CI runs it.
  • The thick-face branch of the integrand (offset_evaluate_simple) still builds an offset surface over a copy per evaluation; the loft has no thick faces.

What follows

Based on main b206d3f5a (after #1006, #992 and #1033); git merge-tree is clean against #1031 (dd47b13b9) and the current main 4fa6661e2. Other callers of Surface::derivs with orders 1, 1 can move to first_derivs_into in a later PR.

Refs #948.

…ray moves (outputs bit-identical)

The volume integrand (OdeIsoCurveLength::evaluate) evaluates the surface
point and first derivatives at every ODE step through
Surface::evaluate_simple -> srf_basic::Surface::derivs. Each call
allocated a BasisDerivs Vec and a Vec<Vec<Vec3>> result, moved two
Result<(isize, [[f64; 8]; 5])> tables out of univariate_derivs, and zeroed
8-wide scratch tables for cubic surfaces.

- univariate_derivs fills the caller's zeroed table (out-param) instead
  of returning it by value.
- derivative_matrix takes the basis derivative as a closure and writes
  into the caller's matrix. derivs keeps its Vec result; the new
  Surface::first_derivs_into (udr = vdr = 1) writes a caller [[Vec3; 2]; 2]
  and, for a non-rational surface, reads Du[k][i] * Dv[l][j] directly (the
  same product BasisDerivs stores). Rational surfaces still go through
  basis_derivs.
- BSplineSurface::evaluate_simple_first_into and
  Surface::evaluate_simple_first_into carry the 2x2 block up; the thin-face
  branch of OdeIsoCurveLength::evaluate uses it. Other kinds fall back to
  evaluate_simple with the same copy semantics.
- Scratch tables are 4 wide (TINY_NP) when both degrees are below 4.

A new test compares first_derivs_into with derivs bit for bit (errors
too) over 7 surfaces (rational, degrees 1 to 8), parameters inside and
outside the domain, both sides and both upper-half modes.

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 26, 2026
Merged via the queue into tangency-labs:main with commit e778982 Oct 26, 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