rsz: replace BfsIterator pull-API usage with local DFS traversal#10862
rsz: replace BfsIterator pull-API usage with local DFS traversal#10862dsengupta0628 wants to merge 4 commits into
Conversation
Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a new test case gcd_restructure_delay and replaces the pull-style sta::BfsFwdIterator and sta::BfsBkwdIterator traversals in Resizer.cc with a custom iterative DFS implementation (dfsSearch). It also updates findClkInverters to sort the discovered inverters by level. The reviewer feedback focuses on optimizing the performance of dfsSearch by templating it on the traversal direction and callback types to eliminate std::function overhead and runtime branching, as well as templating on the seed container type to avoid copying vertex sets into temporary vectors.
| static void dfsSearch(sta::Graph* graph, | ||
| sta::SearchPred& pred, | ||
| DfsDirection dir, | ||
| const std::vector<sta::Vertex*>& seeds, | ||
| bool visit_seeds, | ||
| const std::function<bool(sta::Vertex*)>& visit) | ||
| { | ||
| std::vector<sta::Vertex*> stack; | ||
| sta::VertexSet visited(graph); | ||
|
|
||
| auto expand = [&](sta::Vertex* vertex) { | ||
| if (dir == DfsDirection::fanout) { | ||
| if (pred.searchFrom(vertex)) { | ||
| sta::VertexOutEdgeIterator edge_iter(vertex, graph); | ||
| while (edge_iter.hasNext()) { | ||
| sta::Edge* edge = edge_iter.next(); | ||
| sta::Vertex* to_vertex = edge->to(graph); | ||
| if (pred.searchThru(edge) && pred.searchTo(to_vertex) | ||
| && visited.insert(to_vertex).second) { | ||
| stack.push_back(to_vertex); | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| if (pred.searchTo(vertex)) { | ||
| sta::VertexInEdgeIterator edge_iter(vertex, graph); | ||
| while (edge_iter.hasNext()) { | ||
| sta::Edge* edge = edge_iter.next(); | ||
| sta::Vertex* from_vertex = edge->from(graph); | ||
| if (pred.searchFrom(from_vertex) && pred.searchThru(edge) | ||
| && visited.insert(from_vertex).second) { | ||
| stack.push_back(from_vertex); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| for (sta::Vertex* seed : seeds) { | ||
| if (visit_seeds) { | ||
| if (visited.insert(seed).second) { | ||
| stack.push_back(seed); | ||
| } | ||
| } else { | ||
| expand(seed); | ||
| } | ||
| } | ||
|
|
||
| while (!stack.empty()) { | ||
| sta::Vertex* vertex = stack.back(); | ||
| stack.pop_back(); | ||
| if (visit(vertex)) { | ||
| expand(vertex); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Using std::function for the callback visit in dfsSearch introduces virtual dispatch and potential dynamic allocation overhead in a performance-critical timing graph traversal. Additionally, checking dir inside the hot expansion logic introduces a runtime branch on every visited vertex.
We can optimize this by templating dfsSearch on the SeedsContainer and VisitFunc types, and passing dir as a template parameter. This allows if constexpr to be used to eliminate the runtime branch entirely, and completely avoids std::function overhead. To comply with repository guidelines, the helper logic for expanding vertices is defined as a free function in an anonymous namespace rather than a local lambda.
namespace {
template <DfsDirection dir>
void dfsSearchExpand(sta::Vertex* vertex,
sta::Graph* graph,
sta::SearchPred& pred,
sta::VertexSet& visited,
std::vector<sta::Vertex*>& stack)
{
if constexpr (dir == DfsDirection::fanout) {
if (pred.searchFrom(vertex)) {
sta::VertexOutEdgeIterator edge_iter(vertex, graph);
while (edge_iter.hasNext()) {
sta::Edge* edge = edge_iter.next();
sta::Vertex* to_vertex = edge->to(graph);
if (pred.searchThru(edge) && pred.searchTo(to_vertex)
&& visited.insert(to_vertex).second) {
stack.push_back(to_vertex);
}
}
}
} else {
if (pred.searchTo(vertex)) {
sta::VertexInEdgeIterator edge_iter(vertex, graph);
while (edge_iter.hasNext()) {
sta::Edge* edge = edge_iter.next();
sta::Vertex* from_vertex = edge->from(graph);
if (pred.searchFrom(from_vertex) && pred.searchThru(edge)
&& visited.insert(from_vertex).second) {
stack.push_back(from_vertex);
}
}
}
}
}
} // namespace
template <DfsDirection dir, typename SeedsContainer, typename VisitFunc>
static void dfsSearch(sta::Graph* graph,
sta::SearchPred& pred,
const SeedsContainer& seeds,
bool visit_seeds,
const VisitFunc& visit)
{
std::vector<sta::Vertex*> stack;
stack.reserve(seeds.size());
sta::VertexSet visited(graph);
for (sta::Vertex* seed : seeds) {
if (visit_seeds) {
if (visited.insert(seed).second) {
stack.push_back(seed);
}
} else {
dfsSearchExpand<dir>(seed, graph, pred, visited, stack);
}
}
while (!stack.empty()) {
sta::Vertex* vertex = stack.back();
stack.pop_back();
if (visit(vertex)) {
dfsSearchExpand<dir>(vertex, graph, pred, visited, stack);
}
}
}References
- Helper logic should be defined as a free function in a namespace rather than as a local lambda within a function.
| const std::vector<sta::Vertex*> seeds(ends.begin(), ends.end()); | ||
| dfsSearch( | ||
| graph_, | ||
| pred, | ||
| DfsDirection::fanin, | ||
| seeds, | ||
| /*visit_seeds=*/false, | ||
| [&](sta::Vertex* vertex) { | ||
| if (isRegOutput(vertex) || network_->isTopLevelPort(vertex->pin())) { | ||
| return false; | ||
| } | ||
| fanins.insert(vertex->pin()); | ||
| return true; | ||
| }); |
There was a problem hiding this comment.
By templating dfsSearch on the SeedsContainer type, we can pass ends (which is a sta::VertexSet) directly to dfsSearch without copying its elements into a temporary std::vector.
dfsSearch<DfsDirection::fanin>(
graph_,
pred,
ends,
/*visit_seeds=*/false,
[&](sta::Vertex* vertex) {
if (isRegOutput(vertex) || network_->isTopLevelPort(vertex->pin())) {
return false;
}
fanins.insert(vertex->pin());
return true;
});| const std::vector<sta::Vertex*> seeds(ends.begin(), ends.end()); | ||
| dfsSearch( | ||
| graph_, | ||
| pred, | ||
| DfsDirection::fanin, | ||
| seeds, | ||
| /*visit_seeds=*/false, | ||
| [&](sta::Vertex* vertex) { | ||
| if (isRegOutput(vertex) || network_->isTopLevelPort(vertex->pin())) { | ||
| roots.insert(vertex); | ||
| return false; | ||
| } | ||
| return true; | ||
| }); |
There was a problem hiding this comment.
By templating dfsSearch on the SeedsContainer type, we can pass ends (which is a sta::VertexSet) directly to dfsSearch without copying its elements into a temporary std::vector.
dfsSearch<DfsDirection::fanin>(
graph_,
pred,
ends,
/*visit_seeds=*/false,
[&](sta::Vertex* vertex) {
if (isRegOutput(vertex) || network_->isTopLevelPort(vertex->pin())) {
roots.insert(vertex);
return false;
}
return true;
});| const std::vector<sta::Vertex*> seeds(reg_outs.begin(), reg_outs.end()); | ||
| dfsSearch(graph_, | ||
| pred, | ||
| DfsDirection::fanout, | ||
| seeds, | ||
| /*visit_seeds=*/false, | ||
| [&](sta::Vertex* vertex) { | ||
| if (isRegister(vertex)) { | ||
| return false; | ||
| } | ||
| fanouts.insert(vertex); | ||
| return true; | ||
| }); |
There was a problem hiding this comment.
By templating dfsSearch on the SeedsContainer type, we can pass reg_outs (which is a sta::VertexSet) directly to dfsSearch without copying its elements into a temporary std::vector.
dfsSearch<DfsDirection::fanout>(
graph_,
pred,
reg_outs,
/*visit_seeds=*/false,
[&](sta::Vertex* vertex) {
if (isRegister(vertex)) {
return false;
}
fanouts.insert(vertex);
return true;
});| dfsSearch( | ||
| graph_, | ||
| srch_pred, | ||
| DfsDirection::fanout, | ||
| seeds, | ||
| /*visit_seeds=*/true, | ||
| [&](sta::Vertex* vertex) { | ||
| const sta::Pin* pin = vertex->pin(); | ||
| sta::Instance* inst = network_->instance(pin); | ||
| sta::LibertyCell* lib_cell = network_->libertyCell(inst); | ||
| if (vertex->isDriver(network_) && lib_cell && lib_cell->isInverter()) { | ||
| inverters.emplace_back(vertex->level(), inst); | ||
| debugPrint(logger_, | ||
| RSZ, | ||
| "repair_clk_inverters", | ||
| 2, | ||
| "inverter {}", | ||
| network_->pathName(inst)); | ||
| } | ||
| return !vertex->isRegClk(); | ||
| }); |
There was a problem hiding this comment.
Update the call to dfsSearch to use the template argument for DfsDirection::fanout.
dfsSearch<DfsDirection::fanout>(
graph_,
srch_pred,
seeds,
/*visit_seeds=*/true,
[&](sta::Vertex* vertex) {
const sta::Pin* pin = vertex->pin();
sta::Instance* inst = network_->instance(pin);
sta::LibertyCell* lib_cell = network_->libertyCell(inst);
if (vertex->isDriver(network_) && lib_cell && lib_cell->isInverter()) {
inverters.emplace_back(vertex->level(), inst);
debugPrint(logger_,
RSZ,
"repair_clk_inverters",
2,
"inverter {}",
network_->pathName(inst));
}
return !vertex->isRegClk();
});
Summary
OpenSTA plans to remove BfsIterator::hasNext()/next(), so this PR moves the four remaining rsz users off that API onto a file-local iterative DFS helper in Resizer.cc:
The helper (dfsSearch) reproduces Bfs{Fwd,Bkwd}Iterator::enqueueAdjacentVertices adjacency semantics exactly, including the mode-less SearchPred calls (OR across modes). Explicit stack + visited set: no recursion, terminates on cycles, and drops rsz's
dependence on BfsIndex::other and levelized queue state.
Result sets are unchanged (three functions return unordered sets). findClkInverters now sorts its result by graph level before returning — cloneClkInverter requires upstream-first clone order, previously guaranteed implicitly by BFS level order, now
enforced explicitly.
Also adds rmp/gcd_restructure_delay regression test: delay-mode restructure was the sole caller of findFanins and had zero coverage (existing tests only exercise area mode).
Type of Change
Impact
NA
Verification
./etc/Build.sh).