Skip to content

perf: B-spline curve evaluation into a caller buffer (outputs bit-identical) - #1089

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

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

Conversation

@meros

@meros meros commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

In short: every curve evaluation allocated a small list for its result, and the curve tessellation runs millions of them. The hot caller now passes a fixed buffer instead. Results do not change. On the loft boolean of #948, user instructions fall by about 5 % and cycles by 3 to 4 %.

What is wrong

BSplineCurve::evaluate returns a fresh Vec<Vec3> that nurbs_crv::crv_derivs allocates (Vec::with_capacity(der + 1)) on every call. On the #948 loft (20 sections, main d4940b63e) about 180 profile samples are that allocation. Almost all calls come from FindCurveDistanceExtrema::evaluate (SmFindCurveDistanceExtremaEFO::Evaluate): two evaluations of 3 entries per step of every distance solve in the curve-cache constants. The C++ evaluates into the caller's stack array.

Allocation Rule case Replaced by
Vec<Vec3> per curve evaluation in the distance solve output buffer two [Vec3; 3] on the stack

Change

  • nurbs_crv::crv_derivs_into(cur, u, side, out) writes C^(k)(u) into the caller's slice; crv_derivs allocates and calls it.
  • BSplineCurve::evaluate_into holds the old evaluate body (clamping, zero-tangent repair) over a slice; evaluate allocates and calls it. For num_derivs >= GW_MAX_DERIV it returns NLib's order error as before, without allocating an output of that size.
  • Curve::evaluate_into sends the B-spline kinds (B-spline, hyperbola, ellipse, parabola) to it. Other kinds fall back to evaluate and fill missing entries with Vec3::UNDEF, which is what at() read for them.
  • FindCurveDistanceExtrema::evaluate (in curve_cache.rs) uses two [Vec3; 3] buffers. This is a two-line change in curve_cache.rs. The curve-cache memo (perf: memoise curve caches in the curve value (outputs bit-identical) #992) is already in main and this branch is rebased on it; perf: keep each surface's boundary curves for CoincidenceCheck (outputs bit-identical) #1031 does not touch curve_cache.rs, and git merge-tree is clean against it.

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):

Sections Metric main branch Change
20 instructions (user) 54.87 G 52.08 G -5.1 %
20 cycles (user) 10.84 G 10.48 G -3.3 %
40 instructions (user) 167.59 G 158.34 G -5.5 %
40 cycles (user) 33.53 G 32.21 G -3.9 %

Profile samples (frame-pointer builds, perf record -F 1000, 20 sections, 2 runs each): malloc/free leaves under FindCurveDistanceExtrema::evaluate fall from 12 and 17 to 0 and 0. 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: evaluate_into equals evaluate bit for bit, errors included, for 0 to GW_MAX_DERIV + 1 derivatives, both sides, with and without the zero-tangent repair, on two curves (one with a zero first derivative, so both repair branches run), and an empty output is an error.
  • cargo check -p solidcore --target wasm32-unknown-unknown: pass
  • difftest --unit, all failed=0 deferred=0 max_rel_err=0e0:
PASS nurbs.nurbs_crv cases=4113 passed=4113 failed=0 deferred=0 max_rel_err=0e0
PASS nurbs.b_spline_curve_p1 cases=2216 passed=2216 failed=0 deferred=0 max_rel_err=0e0
PASS curve.curve_p2 cases=3029 passed=3029 failed=0 deferred=0 max_rel_err=0e0
PASS curve.curve_cache cases=642 passed=642 failed=0 deferred=0 max_rel_err=0e0

Not run

  • difftest --green (the CI gate) did not run locally; CI runs it.

What follows

  • Based on main b206d3f5a; git merge-tree is clean against the current main 4fa6661e2.
  • Stacked on this branch: perf/continuity-evaluate-into (evaluate_continuity into stack buffers; separate draft).
  • A 4-wide curve scratch table for cubic curves was tried and is not proposed: instructions -5.4 %, but cycles +6 %.

Refs #948.

…ntical)

BSplineCurve::evaluate returned a fresh Vec<Vec3> from crv_derivs on
every call. On the tangency-labs#948 loft about 180 profile samples are that
allocation, almost all from FindCurveDistanceExtrema::evaluate (two
evaluations of 3 entries per step of every curve-cache distance solve).

- nurbs_crv::crv_derivs_into writes C^(k)(u) into the caller's slice;
  crv_derivs allocates and calls it.
- BSplineCurve::evaluate_into holds the old evaluate body (clamping,
  zero-tangent repair) over a slice; evaluate allocates and calls it, and
  keeps NLib's order error for num_derivs >= GW_MAX_DERIV without
  allocating an output of that size.
- Curve::evaluate_into dispatches the B-spline kinds (B-spline,
  hyperbola, ellipse, parabola) to it; other kinds fall back to evaluate
  and fill missing entries with Vec3::UNDEF, as at() reads them.
- FindCurveDistanceExtrema::evaluate uses two [Vec3; 3] buffers.

A new test compares evaluate_into with evaluate bit for bit, errors
included, for 0 to GW_MAX_DERIV + 1 derivatives, with and without the
zero-tangent repair (one curve has a zero first derivative).

Loft driver, 3 interleaved runs, P-cores, medians: user instructions
-6.1 % and cycles -4.8 % / -4.6 % (20 / 40 sections); all non-timing
output identical.

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