Skip to content

rsz: replace BfsIterator pull-API usage with local DFS traversal#10862

Open
dsengupta0628 wants to merge 8 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:rsz_funcs_dfs
Open

rsz: replace BfsIterator pull-API usage with local DFS traversal#10862
dsengupta0628 wants to merge 8 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:rsz_funcs_dfs

Conversation

@dsengupta0628

Copy link
Copy Markdown
Contributor

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:

  • findFanins
  • findFaninRoots
  • findFanouts
  • findClkInverters

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

  • New feature

Impact

NA

Verification

  • I have verified that the local build succeeds (./etc/Build.sh).
  • I have run the relevant tests and they pass.
  • My code follows the repository's formatting guidelines.
  • I have included tests to prevent regressions.
  • I have signed my commits (DCO).

Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
@dsengupta0628 dsengupta0628 self-assigned this Jul 9, 2026
@github-actions github-actions Bot added the size/M label Jul 9, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread src/rsz/src/Resizer.cc
Comment on lines +3265 to +3320
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);
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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
  1. Helper logic should be defined as a free function in a namespace rather than as a local lambda within a function.

Comment thread src/rsz/src/Resizer.cc
Comment on lines +3337 to +3350
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;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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;
      });

Comment thread src/rsz/src/Resizer.cc
Comment on lines +3359 to +3372
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;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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;
      });

Comment thread src/rsz/src/Resizer.cc
Comment on lines +3394 to +3406
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;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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;
      });

Comment thread src/rsz/src/Resizer.cc
Comment on lines +4974 to +4994
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();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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();
      });

@dsengupta0628 dsengupta0628 marked this pull request as ready for review July 9, 2026 21:17
@dsengupta0628 dsengupta0628 requested review from a team as code owners July 9, 2026 21:17
@dsengupta0628

Copy link
Copy Markdown
Contributor Author

Gemini observations are technically accurate, but I profiled the priorities differently, so I'm declining most of this:

std::function overhead: The indirect call is real but not measurable here. The per-vertex cost of this traversal is dominated by the visited set insert (std::set, O(log n) with a heap allocation per node) and the SearchPred calls, which are genuinely virtual and additionally iterate over all modes in their mode-less variants. On top of that, both call paths are cold: restructure is an opt-in command and repair_clock_inverters runs once per CTS. std::function visitors are also the established pattern in rsz (see RepairTargetCollector.hh, Rebuffer.cc), so I'd rather stay consistent than template for an unmeasurable win.

dir branch in the expansion loop: It's loop-invariant, so it's perfectly predicted after the first iteration - if constexpr would eliminate a branch that's already effectively free.

Anonymous-namespace free function "to comply with repository guidelines": I couldn't find such a guideline; the existing file-local helpers in Resizer.cc (SearchPredCombLogic, ClkArrivalSearchPred) don't use one either.

Where I agree: if we want a performance pass on this helper, the actual lever is the visited container - sta::VertexSet is an ordered std::set, and since it's used purely for membership (never iterated), switching to std::unordered_set<Vertex*> is
determinism-safe and removes the dominant per-edge cost. Templating the seeds parameter to accept any range would also drop the three VertexSet-->vector copies at the call sites. I'd take those two as a follow-up because they're orthogonal to the BFS to DFS migration this PR is scoped to.

@dsengupta0628

Copy link
Copy Markdown
Contributor Author

resizer_dfs_mig.pdf
Spec for this migration

@dsengupta0628

Copy link
Copy Markdown
Contributor Author

@jhkim-pii

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 585da5dfd1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/rmp/test/gcd_restructure_delay.tcl Outdated
set tielo "LOGIC0_X1/Z"

ord::set_thread_count "3"
restructure -liberty_file Nangate45/Nangate45_typ.lib -target delay \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the timing target in the delay regression

In this test, -target delay is not a recognized mode: restructure documents area|timing, and Restructure::setMode only switches to delay optimization for "timing", leaving invalid modes in area mode while warning. The new golden output already includes Mode delay not recognized., so this regression runs the area path (findFaninFanouts) instead of the delay path (findFanins) that this commit is meant to cover, allowing the DFS fanin replacement to regress without the test failing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

actually I now get a seg fault with -target timing in this testcase! Fixing that now before I update this regression

@dsengupta0628

Copy link
Copy Markdown
Contributor Author

Added a new test gcd_restructure_delay.tcl to cover findFanins, which the DFS work rewrote.

findFanins sole caller = rmp::Restructure::getBlob in timing mode (-target timing). Zero ORFS exposure, no existing test.

Existing rmp restructure test gcd_restructure.tcl uses -target area --> hits findFaninRoots/findFanouts (via findFaninFanouts), not findFanins. So before this, findFanins had no test at all.
When I added a test, it exposed a pre-exsting bug

Why Restructure.cpp needs to be changed
vertexWorstSlackPath reads back worst-slack paths that only exist after an arrival/required search; restructure's searchPreamble computes delays only. Timing-mode restructure was never exercised by a test --> nobody hit the missing search + null path.

So we need a force arrival search like this in non area mode:

if (!is_area_mode_) {
   open_sta_->worstSlack(sta::MinMax::max());   // force arrival search
 }

...
sta::Path* path = open_sta_->vertexWorstSlackPath(end_point, ...);
After above line, we also need:
if (path == nullptr) { continue; } // skip unconstrained endpoints

So,

  • worstSlack(max) forces the arrival/required search to run. Without it, every vertexWorstSlackPath returns null (no search data exists) --> total failure.
  • Even after the search, an individual endpoint can have no worst-slack path: unconstrained endpoints (no required time / no clock-relative constraint). the test I added had exactly this - note [WARNING STA-0441] set_input_delay relative to a clock defined on the same port/pin not allowed --> that endpoint's constraint is dropped --> null path.

So: worstSlack() = "search ran at all"; null-guard = "this specific endpoint has no path." Without the guard, PathExpanded expanded(path, ...) derefs null --> segfault. So both fixes needed.

Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants