diff --git a/include/networkit/coarsening/CoarsenedGraphView.hpp b/include/networkit/coarsening/CoarsenedGraphView.hpp index a14c9ea95..684569496 100644 --- a/include/networkit/coarsening/CoarsenedGraphView.hpp +++ b/include/networkit/coarsening/CoarsenedGraphView.hpp @@ -258,6 +258,13 @@ class CoarsenedGraphView { */ const std::vector &getOriginalNodes(node supernode) const; + /** + * Release the calling thread's computeNeighbors() scratch buffer (capacity included). + * Call at the end of a move/refine phase so a giant-supernode high-water mark from one + * level cannot leak into the next. + */ + static void releaseThreadScratch(); + private: const Graph &originalGraph; std::vector nodeMapping; // original_node -> supernode diff --git a/networkit/cpp/coarsening/CoarsenedGraphView.cpp b/networkit/cpp/coarsening/CoarsenedGraphView.cpp index 42f56a9b7..b92f50063 100644 --- a/networkit/cpp/coarsening/CoarsenedGraphView.cpp +++ b/networkit/cpp/coarsening/CoarsenedGraphView.cpp @@ -8,10 +8,24 @@ #include #include -#include +#include +#include +#include namespace NetworKit { +namespace { +// Phase-scoped scratch for computeNeighbors(): reused across calls within a move/refine +// phase, released via releaseThreadScratch() at each phase boundary so a high-water mark +// from one coarsening level cannot leak into the next. +thread_local std::vector> t_staged; +} // namespace + +void CoarsenedGraphView::releaseThreadScratch() { + t_staged.clear(); + t_staged.shrink_to_fit(); +} + CoarsenedGraphView::CoarsenedGraphView(const Graph &originalGraph, const Partition &partition) : originalGraph(originalGraph) { @@ -96,37 +110,54 @@ const std::vector &CoarsenedGraphView::getOriginalNodes(node supernode) co return supernodeToOriginal[supernode]; } -std::vector> -CoarsenedGraphView::computeNeighbors(node supernode) const { - std::unordered_map aggregatedWeights; - - // No locks needed here - supernodeToOriginal and nodeMapping are read-only after construction - // Iterate through all original nodes in this supernode +std::vector> CoarsenedGraphView::computeNeighbors( + node supernode) const { // Flat aggregation (InducedSubgraphView-style): stage (supernode, + // weight) pairs into the + // phase-scoped thread-local buffer, sort, then combine runs. This avoids per-call + // std::unordered_map hashing and its per-node bucket allocations; the only allocation + // left is the returned vector. Sort order also makes the output deterministic. + // Call releaseThreadScratch() at phase boundaries to shrink after every phase. + t_staged.clear(); + + // No locks needed here - supernodeToOriginal and nodeMapping are read-only after + // construction. Iterate through all original nodes in this supernode. for (node originalNode : supernodeToOriginal[supernode]) { // Iterate through neighbors of each original node originalGraph.forNeighborsOf(originalNode, [&](node originalNeighbor, edgeweight weight) { node neighborSupernode = nodeMapping[originalNeighbor]; /* - * An undirected edge sits in the adjacency of both endpoints, so an edge inside this - * supernode would be aggregated twice. Count it once, from the higher endpoint, - * mirroring ParallelPartitionCoarsening's aggregation. + * An undirected edge sits in the adjacency of both endpoints, so an edge inside + * this supernode would be aggregated twice. Count it once, from the higher + * endpoint, mirroring ParallelPartitionCoarsening's aggregation. */ if (neighborSupernode == supernode && originalNode < originalNeighbor) return; - // Aggregate weights to the same supernode - aggregatedWeights[neighborSupernode] += weight; + t_staged.emplace_back(neighborSupernode, weight); }); } - // Convert to vector format - std::vector> neighbors; - neighbors.reserve(aggregatedWeights.size()); + if (t_staged.empty()) + return {}; + + std::sort(t_staged.begin(), t_staged.end(), + [](const auto &a, const auto &b) { return a.first < b.first; }); - for (const auto &entry : aggregatedWeights) { - if (entry.second > 0.0) { // Only include edges with positive weight - neighbors.emplace_back(entry.first, entry.second); + // Combine runs of equal supernodes into the output vector. + std::vector> neighbors; + neighbors.reserve(t_staged.size()); + node cur = t_staged[0].first; + edgeweight acc = 0.0; + for (const auto &entry : t_staged) { + if (entry.first != cur) { + if (acc > 0.0) + neighbors.emplace_back(cur, acc); + cur = entry.first; + acc = 0.0; } + acc += entry.second; } + if (acc > 0.0) // Only include edges with positive weight + neighbors.emplace_back(cur, acc); return neighbors; } diff --git a/networkit/cpp/community/ParallelLeidenView.cpp b/networkit/cpp/community/ParallelLeidenView.cpp index 1c557398f..5eecbee45 100644 --- a/networkit/cpp/community/ParallelLeidenView.cpp +++ b/networkit/cpp/community/ParallelLeidenView.cpp @@ -6,12 +6,15 @@ #include #include +#include #include #ifndef _WIN32 #include #endif +#include #include -#include +#include +#include namespace NetworKit { @@ -433,9 +436,13 @@ ParallelLeidenView::MoveStats ParallelLeidenView::parallelMove(const GraphType & currentNodes.reserve(tshare); std::vector newNodes; newNodes.reserve(WORKING_SIZE); - // Sparse cutWeight[Community] storage avoids one dense community vector per thread. - std::unordered_map cutWeights; + // Flat cut-weight storage (InducedSubgraphView-style): stage (community, weight) + // pairs once per visit, sort, and combine runs into parallel pointers/cutVals + // vectors. Binary search replaces hash lookups; per-thread staging vectors are + // reused across visits to avoid malloc/free churn. + std::vector> staged; std::vector pointers; + std::vector cutVals; auto moveLimitReached = [&](node candidate) { return movesPerNode[candidate].load() >= static_cast(maxMovesPerNode); @@ -538,51 +545,61 @@ ParallelLeidenView::MoveStats ParallelLeidenView::parallelMove(const GraphType & index bestCommunity = none; double degree = 0; count nodeMass = nodeSize(graph, u); - for (auto z : pointers) { - // Reset the clearlist : Set all cutweights to 0 and clear the pointer vector - cutWeights.erase(z); - } + staged.clear(); pointers.clear(); + cutVals.clear(); graph.forNeighborsOf(u, [&](node neighbor, edgeweight ew) { - index neighborCommunity = result[neighbor]; - if (cutWeights.find(neighborCommunity) == cutWeights.end()) { - pointers.push_back(neighborCommunity); - } if (u == neighbor) { - degree += ew; + degree += 2 * ew; } else { - cutWeights[neighborCommunity] += ew; + staged.emplace_back(result[neighbor], ew); + degree += ew; // keep track of the nodes degree. Loops count twice } - degree += ew; // keep track of the nodes degree. Loops count twice }); - if (pointers.empty()) { + if (staged.empty()) { finishProcessingNode(u); continue; } + // Sort + combine runs: one entry per adjacent community. + std::sort(staged.begin(), staged.end(), + [](const auto &a, const auto &b) { return a.first < b.first; }); + for (const auto &entry : staged) { + if (!pointers.empty() && pointers.back() == entry.first) { + cutVals.back() += entry.second; + } else { + pointers.push_back(entry.first); + cutVals.push_back(entry.second); + } + } + staged.clear(); + + auto cutOf = [&](index community) { + auto it = std::lower_bound(pointers.begin(), pointers.end(), community); + if (it == pointers.end() || *it != community) + return 0.0; + return cutVals[static_cast(it - pointers.begin())]; + }; + double singletonScore = scoreCommunity(0.0, degree, 0.0, nodeMass, 0); // Determine move score for all neighbor communities - for (auto community : pointers) { + for (size_t k = 0; k < pointers.size(); ++k) { + index community = pointers[k]; // "Moving" a node to its current community is pointless if (community != currentCommunity) { - double delta; - const auto cutWeightIt = cutWeights.find(community); - const double cutWeight = - cutWeightIt == cutWeights.end() ? 0.0 : cutWeightIt->second; - delta = scoreCommunity(cutWeight, degree, communityVolumes[community], - nodeMass, communitySizes[community]); + double delta = + scoreCommunity(cutVals[k], degree, communityVolumes[community], + nodeMass, communitySizes[community]); if (delta > maxDelta) { maxDelta = delta; bestCommunity = community; } } } - const auto currentCutWeightIt = cutWeights.find(currentCommunity); - const double currentCutWeight = - currentCutWeightIt == cutWeights.end() ? 0.0 : currentCutWeightIt->second; + const double currentCutWeight = cutOf(currentCommunity); double modThreshold = scoreCurrentCommunityThreshold( currentCutWeight, degree, communityVolumes[currentCommunity], nodeMass, communitySizes[currentCommunity]); @@ -693,6 +710,9 @@ ParallelLeidenView::MoveStats ParallelLeidenView::parallelMove(const GraphType & TRACE("Thread ", omp_get_thread_num(), " worked ", totalNodesPerThread[omp_get_thread_num()], "Nodes and moved ", moved[omp_get_thread_num()]); + // Shrink after every phase: drop this thread's view-aggregation scratch so a + // high-water mark from this level cannot leak into the next. + CoarsenedGraphView::releaseThreadScratch(); } result.setUpperBound(upperBound); assert(queue.empty()); @@ -760,8 +780,9 @@ Partition ParallelLeidenView::parallelRefine(const GraphType &graph, bool &refin #pragma omp parallel { std::vector neighComms; - // Keeps track of relevant Neighbor communities. Needed to reset the clearlist fast - std::unordered_map cutWeights; // cut from Node to Communities + // Flat cut map (InducedSubgraphView-style): sorted-vector storage instead of a hash + // table; refinement neighborhoods are small so binary search beats hashing. + Aux::flat_map cutWeights; // cut from Node to Communities auto &mt = Aux::Random::getURNG(); #pragma omp for for (omp_index u = 0; u < static_cast(graph.upperNodeIdBound()); u++) { @@ -958,6 +979,8 @@ Partition ParallelLeidenView::parallelRefine(const GraphType &graph, bool &refin locks[bestC].unlock(); locks[u].unlock(); } + // Shrink after every phase: drop this thread's view-aggregation scratch. + CoarsenedGraphView::releaseThreadScratch(); } refineMadeChanges = anyRefinementChanges.load();