Repository navigation
perf: first-derivative surface evaluation without per-call heap or array moves (outputs bit-identical) - #1087
Merged
Conversation
…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
approved these changes
Oct 26, 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.
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.
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:BasisDerivsVecand aVec<Vec<Vec3>>result;Result<(isize, [[f64; 8]; 5])>tables out ofunivariate_derivs(amemmoveeach);The C++ (
N_SrfDerivs) uses stack arrays and the caller's output array.univariate_derivsresult returned by valueBasisDerivsVecandVec<Vec<Vec3>>per 2x2 evaluationfirst_derivs_intowrites the caller's[[Vec3; 2]; 2]Change
univariate_derivswrites into the caller's zeroed table.derivative_matrixtakes the basis derivative as a closure and writes into the caller's matrix.Surface::derivskeeps itsVecresult for every other caller.Surface::first_derivs_into(orders 1, 1) writes a[[Vec3; 2]; 2]. For a non-rational surface it readsDu[k][i] * Dv[l][j]straight from the two univariate tables: the same productbasis_derivsstores, so the same bits. Rational surfaces still go throughbasis_derivs.BSplineSurface::evaluate_simple_first_intoandSurface::evaluate_simple_first_intocarry the 2x2 block up. Kinds that are not B-spline based fall back toevaluate_simplewith the same copy semantics as before. The thin-face branch ofOdeIsoCurveLength::evaluateuses it.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, mainb206d3f5aagainst 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 %):Volume-phase profile samples (frame-pointer builds of the same two trees,
perf record -F 1000, 20 sections, 2 runs each):brep_volumeOdeIsoCurveLength::evaluatememmove/memset/malloc/freeleavesAt this speed the per-step
volume tooktimes (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: passcargo clippy --workspace --all-targets -- -D warnings: passcargo test -p solidcore: 2275 passed, 0 failed. New test:first_derivs_intoequalsderivsbit 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--unit, allfailed=0 deferred=0 max_rel_err=0e0:Not run
difftest --green(the CI gate) did not run locally; CI runs it.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-treeis clean against #1031 (dd47b13b9) and the current main4fa6661e2. Other callers ofSurface::derivswith orders 1, 1 can move tofirst_derivs_intoin a later PR.Refs #948.