Skip to content

Commit 3562963

Browse files
Multicut performance improvements
1 parent 059c263 commit 3562963

3 files changed

Lines changed: 186 additions & 126 deletions

File tree

include/bioimage_cpp/graph/multicut/detail.hxx

Lines changed: 160 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,107 @@
11
#pragma once
22

3+
#include "bioimage_cpp/detail/indexed_heap.hxx"
34
#include "bioimage_cpp/detail/union_find.hxx"
45
#include "bioimage_cpp/graph/connected_components.hxx"
56
#include "bioimage_cpp/graph/multicut/objective.hxx"
67
#include "bioimage_cpp/graph/undirected_graph.hxx"
78

8-
#include <algorithm>
99
#include <cmath>
1010
#include <cstddef>
1111
#include <cstdint>
12-
#include <queue>
12+
#include <limits>
1313
#include <random>
14-
#include <unordered_map>
15-
#include <unordered_set>
1614
#include <utility>
1715
#include <vector>
1816

1917
namespace bioimage_cpp::graph::multicut::detail {
2018

19+
inline constexpr std::size_t no_edge = std::numeric_limits<std::size_t>::max();
20+
21+
// Per-super-node adjacency entry.
22+
struct NeighborEntry {
23+
std::size_t neighbor;
24+
std::size_t edge_id;
25+
};
26+
27+
// Per-edge data stored in a flat vector indexed by stable edge_id. Edge ids
28+
// never grow past the original number of input edges — when two edges fold
29+
// into one during a merge, one id survives and the other becomes orphaned
30+
// (no adjacency entry or heap entry references it again).
2131
struct DynamicEdge {
32+
std::size_t u = 0;
33+
std::size_t v = 0;
2234
double weight = 0.0;
23-
std::uint64_t edition = 0;
35+
unsigned char is_constraint = 0;
2436
};
2537

26-
struct DynamicGraph {
27-
explicit DynamicGraph(const std::size_t size) : adjacency(size), constraints(size), alive(size, true), alive_count(size) {
28-
}
38+
// Heap keyed by dense stable edge_id. The vector-backed DenseLocator updates
39+
// in O(1) per sift step, which was the missing ingredient for greedy_additive
40+
// to beat the std::priority_queue baseline.
41+
using EdgeHeap = bioimage_cpp::detail::DenseIndexedHeap<double>;
2942

43+
struct DynamicGraph {
3044
explicit DynamicGraph(const UndirectedGraph &graph)
3145
: adjacency(static_cast<std::size_t>(graph.number_of_nodes())),
32-
constraints(static_cast<std::size_t>(graph.number_of_nodes())),
3346
alive(static_cast<std::size_t>(graph.number_of_nodes()), true),
34-
alive_count(static_cast<std::size_t>(graph.number_of_nodes())) {
47+
alive_count(static_cast<std::size_t>(graph.number_of_nodes())),
48+
scratch_edge_id(static_cast<std::size_t>(graph.number_of_nodes()), no_edge) {
3549
for (std::uint64_t node = 0; node < graph.number_of_nodes(); ++node) {
3650
const auto degree = graph.node_adjacency(node).size();
3751
adjacency[static_cast<std::size_t>(node)].reserve(degree);
3852
}
53+
edges.resize(static_cast<std::size_t>(graph.number_of_edges()));
3954
}
4055

41-
[[nodiscard]] bool edge_exists(const std::size_t u, const std::size_t v) const {
42-
return alive[u] && alive[v] && adjacency[u].find(v) != adjacency[u].end();
43-
}
44-
45-
[[nodiscard]] const DynamicEdge *edge(const std::size_t u, const std::size_t v) const {
46-
if (!alive[u] || !alive[v]) {
47-
return nullptr;
56+
// O(degree(u)). Returns no_edge when (u, v) is not an edge.
57+
[[nodiscard]] std::size_t find_edge(const std::size_t u, const std::size_t v) const {
58+
if (u >= adjacency.size() || v >= adjacency.size()) {
59+
return no_edge;
4860
}
49-
const auto found = adjacency[u].find(v);
50-
return found == adjacency[u].end() ? nullptr : &found->second;
51-
}
52-
53-
[[nodiscard]] bool has_constraint(const std::size_t u, const std::size_t v) const {
54-
return constraints[u].find(v) != constraints[u].end();
55-
}
56-
57-
void add_constraint(const std::size_t u, const std::size_t v) {
58-
constraints[u].insert(v);
59-
constraints[v].insert(u);
60-
}
61-
62-
void set_initial_edge(const std::size_t u, const std::size_t v, const double weight) {
63-
adjacency[u][v] = DynamicEdge{weight, 0};
64-
adjacency[v][u] = DynamicEdge{weight, 0};
61+
for (const auto &entry : adjacency[u]) {
62+
if (entry.neighbor == v) {
63+
return entry.edge_id;
64+
}
65+
}
66+
return no_edge;
6567
}
6668

67-
std::uint64_t update_edge(const std::size_t u, const std::size_t v, const double weight) {
68-
const auto current = adjacency[u].find(v);
69-
const auto edition = current == adjacency[u].end() ? 0 : current->second.edition + 1;
70-
adjacency[u][v] = DynamicEdge{weight, edition};
71-
adjacency[v][u] = DynamicEdge{weight, edition};
72-
return edition;
69+
[[nodiscard]] bool edge_exists(const std::size_t u, const std::size_t v) const {
70+
return find_edge(u, v) != no_edge;
7371
}
7472

75-
void remove_node(const std::size_t node) {
76-
for (const auto &entry : adjacency[node]) {
77-
adjacency[entry.first].erase(node);
78-
constraints[entry.first].erase(node);
79-
}
80-
adjacency[node].clear();
81-
constraints[node].clear();
82-
alive[node] = false;
83-
--alive_count;
73+
[[nodiscard]] bool has_constraint(const std::size_t u, const std::size_t v) const {
74+
const auto id = find_edge(u, v);
75+
return id != no_edge && edges[id].is_constraint != 0;
8476
}
8577

86-
std::vector<std::unordered_map<std::size_t, DynamicEdge>> adjacency;
87-
std::vector<std::unordered_set<std::size_t>> constraints;
78+
std::vector<std::vector<NeighborEntry>> adjacency;
79+
std::vector<DynamicEdge> edges;
8880
std::vector<bool> alive;
8981
std::size_t alive_count;
82+
// Sized to #super-nodes, all entries == no_edge between merges. Used by
83+
// merge_dynamic_nodes to find the existing stable-side edge for each of
84+
// removed's neighbors in O(1) without hashing.
85+
std::vector<std::size_t> scratch_edge_id;
9086
};
9187

92-
struct QueueEdge {
93-
std::size_t u = 0;
94-
std::size_t v = 0;
95-
double priority = 0.0;
96-
std::uint64_t edition = 0;
97-
98-
bool operator<(const QueueEdge &other) const {
99-
return priority < other.priority;
100-
}
101-
};
88+
inline double priority_for(const double weight, const bool absolute_priority) {
89+
return absolute_priority ? std::abs(weight) : weight;
90+
}
10291

10392
inline void initialize_dynamic_graph(
10493
const UndirectedGraph &graph,
10594
const std::vector<double> &costs,
10695
DynamicGraph &dynamic_graph,
107-
std::priority_queue<QueueEdge> &queue,
96+
EdgeHeap &heap,
10897
const bool absolute_priority,
10998
const bool add_noise = false,
11099
const int seed = 42,
111100
const double sigma = 1.0
112101
) {
102+
const auto n_edges = static_cast<std::size_t>(graph.number_of_edges());
103+
heap.reset_capacity(n_edges);
104+
113105
std::mt19937 generator(seed);
114106
std::normal_distribution<double> noise(0.0, sigma);
115107
for (std::uint64_t edge = 0; edge < graph.number_of_edges(); ++edge) {
@@ -120,8 +112,15 @@ inline void initialize_dynamic_graph(
120112
if (add_noise) {
121113
weight += noise(generator);
122114
}
123-
dynamic_graph.set_initial_edge(u, v, weight);
124-
queue.push(QueueEdge{u, v, absolute_priority ? std::abs(weight) : weight, 0});
115+
const auto edge_id = static_cast<std::size_t>(edge);
116+
auto &e = dynamic_graph.edges[edge_id];
117+
e.u = u;
118+
e.v = v;
119+
e.weight = weight;
120+
e.is_constraint = 0;
121+
dynamic_graph.adjacency[u].push_back({v, edge_id});
122+
dynamic_graph.adjacency[v].push_back({u, edge_id});
123+
heap.push(edge_id, priority_for(weight, absolute_priority));
125124
}
126125
}
127126

@@ -138,10 +137,45 @@ inline std::size_t stop_node_count(const UndirectedGraph &graph, const double no
138137
: static_cast<std::size_t>(double(graph.number_of_nodes()) * node_num_stop + 0.5);
139138
}
140139

140+
namespace internal {
141+
142+
// Erase the first entry with neighbor == target from `list` (swap-with-back).
143+
// Returns whether anything was erased.
144+
inline bool erase_by_neighbor(std::vector<NeighborEntry> &list, const std::size_t target) {
145+
for (std::size_t i = 0; i < list.size(); ++i) {
146+
if (list[i].neighbor == target) {
147+
list[i] = list.back();
148+
list.pop_back();
149+
return true;
150+
}
151+
}
152+
return false;
153+
}
154+
155+
// Rename the first entry whose neighbor == from_node to point to to_node.
156+
inline void rename_neighbor(
157+
std::vector<NeighborEntry> &list,
158+
const std::size_t from_node,
159+
const std::size_t to_node
160+
) {
161+
for (auto &entry : list) {
162+
if (entry.neighbor == from_node) {
163+
entry.neighbor = to_node;
164+
return;
165+
}
166+
}
167+
}
168+
169+
} // namespace internal
170+
171+
// Contract the edge between u and v in the dynamic graph. The smaller-degree
172+
// super-node is folded into the larger-degree one to keep the per-super-node
173+
// adjacency growth amortized. The heap is kept in sync without staleness: each
174+
// edge id appears at most once.
141175
inline std::size_t merge_dynamic_nodes(
142176
DynamicGraph &dynamic_graph,
143177
bioimage_cpp::detail::UnionFind &sets,
144-
std::priority_queue<QueueEdge> &queue,
178+
EdgeHeap &heap,
145179
std::size_t u,
146180
std::size_t v,
147181
const bool absolute_priority
@@ -151,36 +185,79 @@ inline std::size_t merge_dynamic_nodes(
151185
if (u == v) {
152186
return u;
153187
}
188+
154189
auto stable = u;
155190
auto removed = v;
156191
if (dynamic_graph.adjacency[stable].size() < dynamic_graph.adjacency[removed].size()) {
157192
std::swap(stable, removed);
158193
}
159194
sets.merge_to(stable, removed);
160-
std::vector<std::pair<std::size_t, DynamicEdge>> neighbors(
161-
dynamic_graph.adjacency[removed].begin(),
162-
dynamic_graph.adjacency[removed].end()
163-
);
164-
dynamic_graph.adjacency[stable].erase(removed);
165-
dynamic_graph.constraints[stable].erase(removed);
166-
for (const auto &[neighbor, removed_edge] : neighbors) {
195+
196+
// Stamp stable's neighbors so each removed-neighbor lookup is O(1).
197+
for (const auto &entry : dynamic_graph.adjacency[stable]) {
198+
dynamic_graph.scratch_edge_id[entry.neighbor] = entry.edge_id;
199+
}
200+
201+
// The contracted edge (stable, removed) must be in stable's adjacency.
202+
const auto contracted_edge_id = dynamic_graph.scratch_edge_id[removed];
203+
heap.erase(contracted_edge_id);
204+
dynamic_graph.scratch_edge_id[removed] = no_edge;
205+
internal::erase_by_neighbor(dynamic_graph.adjacency[stable], removed);
206+
207+
// Snapshot removed's neighbors before mutating its adjacency.
208+
const auto removed_neighbors = dynamic_graph.adjacency[removed];
209+
210+
for (const auto &entry : removed_neighbors) {
211+
const auto neighbor = entry.neighbor;
212+
const auto removed_edge_id = entry.edge_id;
167213
if (neighbor == stable) {
168214
continue;
169215
}
170-
const auto current = dynamic_graph.edge(stable, neighbor);
171-
const auto merged_weight = (current == nullptr ? 0.0 : current->weight) + removed_edge.weight;
172-
if (dynamic_graph.has_constraint(removed, neighbor)) {
173-
dynamic_graph.add_constraint(stable, neighbor);
216+
217+
const auto existing_id = dynamic_graph.scratch_edge_id[neighbor];
218+
if (existing_id == no_edge) {
219+
// No existing stable-side edge to `neighbor`. Re-key the
220+
// removed-side edge by replacing `removed` with `stable` on both
221+
// sides. The edge_id, weight and constraint flag are preserved,
222+
// so the heap entry remains valid without any priority change.
223+
dynamic_graph.adjacency[stable].push_back({neighbor, removed_edge_id});
224+
dynamic_graph.scratch_edge_id[neighbor] = removed_edge_id;
225+
internal::rename_neighbor(dynamic_graph.adjacency[neighbor], removed, stable);
226+
auto &e = dynamic_graph.edges[removed_edge_id];
227+
if (e.u == removed) {
228+
e.u = stable;
229+
} else {
230+
e.v = stable;
231+
}
232+
} else {
233+
// Stable already has an edge to `neighbor`; fold removed's weight
234+
// (and constraint flag) into it. Then drop the removed-side edge.
235+
auto &keep = dynamic_graph.edges[existing_id];
236+
const auto &fold = dynamic_graph.edges[removed_edge_id];
237+
keep.weight += fold.weight;
238+
const bool propagated_constraint =
239+
keep.is_constraint != 0 || fold.is_constraint != 0;
240+
keep.is_constraint = propagated_constraint ? 1 : 0;
241+
242+
heap.erase(removed_edge_id);
243+
internal::erase_by_neighbor(dynamic_graph.adjacency[neighbor], removed);
244+
245+
if (propagated_constraint) {
246+
heap.erase(existing_id);
247+
} else {
248+
heap.change(existing_id, priority_for(keep.weight, absolute_priority));
249+
}
174250
}
175-
const auto edition = dynamic_graph.update_edge(stable, neighbor, merged_weight);
176-
queue.push(QueueEdge{
177-
std::min(stable, neighbor),
178-
std::max(stable, neighbor),
179-
absolute_priority ? std::abs(merged_weight) : merged_weight,
180-
edition
181-
});
182251
}
183-
dynamic_graph.remove_node(removed);
252+
253+
// Clear scratch via the updated stable adjacency (includes appended entries).
254+
for (const auto &entry : dynamic_graph.adjacency[stable]) {
255+
dynamic_graph.scratch_edge_id[entry.neighbor] = no_edge;
256+
}
257+
258+
dynamic_graph.adjacency[removed].clear();
259+
dynamic_graph.alive[removed] = false;
260+
--dynamic_graph.alive_count;
184261
return stable;
185262
}
186263

include/bioimage_cpp/graph/multicut/greedy_additive.hxx

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
#include "bioimage_cpp/graph/multicut/detail.hxx"
44
#include "bioimage_cpp/graph/multicut/objective.hxx"
55

6-
#include <algorithm>
76
#include <cstddef>
87
#include <cstdint>
9-
#include <queue>
10-
#include <unordered_map>
118
#include <vector>
129

1310
namespace bioimage_cpp::graph::multicut {
@@ -24,26 +21,20 @@ inline std::vector<std::uint64_t> greedy_additive(
2421
validate_costs(graph, costs);
2522
detail::DynamicGraph dynamic_graph(graph);
2623
bioimage_cpp::detail::UnionFind sets(static_cast<std::size_t>(graph.number_of_nodes()));
27-
std::priority_queue<detail::QueueEdge> queue;
28-
detail::initialize_dynamic_graph(graph, costs, dynamic_graph, queue, false, add_noise, seed, sigma);
24+
detail::EdgeHeap heap;
25+
detail::initialize_dynamic_graph(graph, costs, dynamic_graph, heap, false, add_noise, seed, sigma);
2926

30-
while (!queue.empty() && dynamic_graph.alive_count > 1) {
31-
auto edge = queue.top();
32-
queue.pop();
33-
const auto u = std::min(edge.u, edge.v);
34-
const auto v = std::max(edge.u, edge.v);
35-
const auto *dynamic_edge = dynamic_graph.edge(u, v);
36-
if (dynamic_edge == nullptr || edge.edition < dynamic_edge->edition) {
37-
continue;
38-
}
39-
const auto weight = dynamic_edge->weight;
40-
if (weight <= weight_stop) {
27+
while (!heap.empty() && dynamic_graph.alive_count > 1) {
28+
const auto top = heap.top();
29+
if (top.priority <= weight_stop) {
4130
break;
4231
}
43-
if (node_num_stop > 0.0 && dynamic_graph.alive_count <= detail::stop_node_count(graph, node_num_stop)) {
32+
if (node_num_stop > 0.0
33+
&& dynamic_graph.alive_count <= detail::stop_node_count(graph, node_num_stop)) {
4434
break;
4535
}
46-
detail::merge_dynamic_nodes(dynamic_graph, sets, queue, u, v, false);
36+
const auto &edge = dynamic_graph.edges[top.key];
37+
detail::merge_dynamic_nodes(dynamic_graph, sets, heap, edge.u, edge.v, false);
4738
}
4839
return detail::labels_from_sets(sets, graph);
4940
}

0 commit comments

Comments
 (0)