countPinched: flat buffers instead of a map and a set per vertex - #179
Merged
Merged
Conversation
revarbat
changed the base branch from
perf/export-split-overlap-only
to
main
September 16, 2026 00:51
checkMesh walks the link of every vertex to find pinches. It did that by
building a std::unordered_map of std::vectors and a std::set FRESH FOR EACH
VERTEX, to answer a question about roughly six neighbours -- on the order
of a dozen allocations per vertex, millions on a 200K-triangle mesh.
The buffers are now hoisted out of the loop and cleared per vertex, the
link is a flat node list with linear lookup (a scan beats hashing outright
at that size, and allocates nothing), and connectivity is union-find with
path halving rather than a set-based walk.
`vertTris` went the same way: it was a vector-of-vectors, one allocation
per vertex, and it is only ever read back as a flat run. It is CSR now,
counted during the scan that was already running and filled in one pass
afterwards.
This is the same fix the file's own comment describes applying to the edge
and face counts -- "allocated a tree node per EDGE and per FACE ... to
compute a handful of counters" -- which left the vertex link untouched.
countPinched, measured in isolation:
before after
Dalek 65.2ms 7.3ms 8.9x
100 96.3ms 8.2ms 11.7x
It was 63% of checkMesh. Whole-export effect, on top of #177:
stl 3mf
Dalek 0.611 -> 0.546 2.06 -> 1.93
100 spheres 0.895 -> 0.797 1.83 -> 1.63
Export output is byte-identical -- checkMesh only produces warning text and
never touches geometry -- verified by fingerprinting STL bytes and the 3MF
mesh with and without this change.
ThreeTetrahedraOnOneVertexAreStillOnePinchedVertex pins the one thing the
rewrite could plausibly get wrong: the count is of pinched VERTICES, so a
link in three pieces is still 1, and an implementation that counted pieces
would agree with the existing two-piece test by luck. It passes on the old
code too; it is an equivalence guard, not a bug it caught.
revarbat
force-pushed
the
perf/checkmesh-pinched-allocations
branch
from
September 16, 2026 00:52
c4710c5 to
069e8fa
Compare
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.
Stacked on #178 (based on
perf/export-split-overlap-only), so review that one first.What
With #177 fixed, I profiled what was left of export. For a single-mesh format the split is:
The merge is Manifold's own
BatchBooleanand is real work.stripSliversis already negligible. InsidecheckMesh, though:countPinchedis 63% of the check.Why it was slow
It builds a
std::unordered_map<Vert, std::vector<Vert>>and astd::set<Vert>fresh for each vertex, to answer a question about roughly six neighbours. That is on the order of a dozen allocations per vertex — millions on a 200K-triangle mesh — for a graph that fits in a cache line.This is the same problem the file already documents fixing one function below:
The edge and face counts got that treatment. The vertex link did not.
The change
std::set-based walk.vertTriswas a vector-of-vectors — one allocation per vertex, only ever read back as a flat run. It is CSR now: counted during the scan that was already running, filled in one pass afterwards.Results
countPinchedin isolation:Whole export, on top of #178:
It also speeds up every
import()of a mesh andpolyhedron()'s manifoldness probe, both of which callcheckMesh.Output is unchanged
checkMeshonly produces warning text; it never touches geometry. Verified rather than assumed: STL bytes, object counts, triangle counts and the 3MF mesh hash are identical with and without this change across Dalek, CosBox, Anklet and the 100-sphere model.Tests
MeshCheck.ThreeTetrahedraOnOneVertexAreStillOnePinchedVertexpins the one thing the rewrite could plausibly get wrong: the count is of pinched vertices, so a link in three pieces is still 1. An implementation that counted pieces instead would return 2 and still agree with the existing two-piece test by luck.It passes on the old code too — an equivalence guard, not a bug it caught. The speed claim rests on the isolated measurements above.
Full C++ suite: 1263 passed. Python bindings: 42 passed.
🤖 Generated with Claude Code