Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optimized connected component computation #569

Merged
merged 3 commits into from
Oct 5, 2023

Conversation

pca006132
Copy link
Collaborator

From #561 (reply in thread):

Connected components is definitely slow - I looked for quite a while for a decent parallel solution, but couldn't find anything for general graphs. The trouble is they mostly scale with the diameter of the graph, which for us is ~O(n^1/2), which is pretty bad. I wonder if we could create faces by hashing their normal vector and origin offset into precision-sized buckets? It would relate disconnected parts of the same plane, but I don't think that's a problem (that's what Boolean ops will do anyway).

I was thinking about this yesterday, and decided to look at graphlite's connected component algorithm. The algorithm in graphlite and boost graph are using hashmap, which is usually slower than union find. So I implemented union find and found about 50% performance improvement for larger graphs, which is pretty nice.
Further checking shows that there are many vertices with no neighbors, which probably correspond to faces with no coplanar neighbors. Optimizing that by not storing their ID in a hashmap provided further performance improvement.

Optimizing can speed up large mesh import significantly. Here are some of the timings in our test cases (I only printed the timing for GetLabels when numNodes > 10000 to avoid showing too many results)

           master                               this patch
Manifold.MeshRelation (11 ms)            Manifold.MeshRelation (7 ms)
took 1084us                              took 103us
                                         
Manifold.TictacHull (741 ms)             Manifold.TictacHull (652 ms)
took 67944us                             took 16109us
                                         
Manifold.HollowHull (79 ms)              Manifold.HollowHull (67 ms)
took 3990us                              took 382us
                                         
Boolean.MeshRelation (25 ms)             Boolean.MeshRelation (18 ms)
took 1817us                              took 455us
took 742us                               took 45us
                                         
SDF.Resize (37 ms)                       SDF.Resize (28 ms)
took 8809us                              took 1835us
                                         
Samples.Knot13 (15 ms)                   Samples.Knot13 (8 ms)
took 3787us                              took 1371us
took 1392us                              took 104us
                                         
Samples.Scallop (124 ms)                 Samples.Scallop (47 ms)
took 23361us                             took 657us
took 6084us                              took 512us
took 21652us                             took 759us
took 6219us                              took 482us
                                         
Samples.Bracelet (182 ms)                Samples.Bracelet (170 ms)
took 3189us                              took 620us
took 3852us                              took 645us
                                         
Samples.GyroidModule (305 ms)            Samples.GyroidModule (255 ms)
took 16467us                             took 6001us
took 19764us                             took 6162us
                                         
Samples.SelfIntersect (30 ms)            Samples.SelfIntersect (24 ms)
took 2085us                              took 626us
took 2519us                              took 571us

this also removes graphlite from our dependencies.

@pca006132 pca006132 requested a review from elalish October 5, 2023 14:50
@codecov
Copy link

codecov bot commented Oct 5, 2023

Codecov Report

All modified lines are covered by tests ✅

Comparison is base (206871a) 91.13% compared to head (ab06df4) 91.18%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #569      +/-   ##
==========================================
+ Coverage   91.13%   91.18%   +0.04%     
==========================================
  Files          35       35              
  Lines        4547     4570      +23     
==========================================
+ Hits         4144     4167      +23     
  Misses        403      403              
Files Coverage Δ
src/manifold/src/constructors.cpp 96.39% <100.00%> (-0.02%) ⬇️
src/manifold/src/impl.cpp 96.19% <100.00%> (-0.03%) ⬇️
src/manifold/src/sort.cpp 89.45% <100.00%> (-0.17%) ⬇️
src/utilities/include/utils.h 76.92% <100.00%> (+21.04%) ⬆️

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Owner

@elalish elalish left a comment

Choose a reason for hiding this comment

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

Wow, I can't believe how simple this is! I had to look up Union Find to understand what's going on here. That's excellent, especially the removal of a whole dependency and so much less code!

}

mergeToVert.clear();
mergeFromVert.clear();
for (int v = 0; v < numVert; ++v) {
const int mergeTo = label2vert[vertLabels[v]];
const int mergeTo = uf.find(v);
Copy link
Owner

Choose a reason for hiding this comment

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

Nice! Does this mean we can remove label2vert entirely?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, find always return one representative

I connectedComponents(std::vector<I>& components) {
components.resize(parents.size());
I lonelyNodes = 0;
std::unordered_map<I, I> toLabel;
Copy link
Owner

Choose a reason for hiding this comment

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

I can't remember if this is true everywhere, but I recall being annoyed that connected components returned arbitrary sequential labels instead of a representative member, so I ended up having to add code to get back to a representative member. It may be possible that the connected components part of this isn't actually necessary and that find may be all we really need.

Copy link
Collaborator Author

@pca006132 pca006132 Oct 5, 2023

Choose a reason for hiding this comment

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

I didn't spend much time looking at the code so I only changed the algorithm for MeshGL::Merge that I can immediately recognize.

It will be much faster if you can just work with a representative member instead of requiring a sequential component ID (and don't need the component count). This connectedComponent is a lot slower than the union part. In fact, it probably took around 80% of the time in GetLabel.

@pca006132 pca006132 merged commit d51ee70 into elalish:master Oct 5, 2023
18 checks passed
@elalish elalish mentioned this pull request Nov 3, 2023
cartesian-theatrics pushed a commit to SovereignShop/manifold that referenced this pull request Mar 11, 2024
* use union find for graph connected components

* update readme

* remove graphlite dep
@pca006132 pca006132 deleted the optimizations3 branch November 16, 2024 16:31
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