Skip to content

[dbSta] driver cache: key by canonical flat dbNet (root-cause fix for #10327)#10344

Closed
oharboe wants to merge 1 commit into
The-OpenROAD-Project:masterfrom
oharboe:dbsta-canonical-driver-cache
Closed

[dbSta] driver cache: key by canonical flat dbNet (root-cause fix for #10327)#10344
oharboe wants to merge 1 commit into
The-OpenROAD-Project:masterfrom
oharboe:dbsta-canonical-driver-cache

Conversation

@oharboe
Copy link
Copy Markdown
Collaborator

@oharboe oharboe commented May 6, 2026

Summary

Follow-up to #10327, addressing review feedback from @povik:

Would Claude be able to find and fix the root cause of needing to do the
quadratic-shaped work? Without introducing specialized APIs and while
maintaining correctness of the existing APIs.

This PR fixes the root cause in dbNetwork::disconnectPinBefore so plain
sta::deleteInstance is fast on hierarchical designs without any new
"begin/end bulk delete" API. Closes #10329.

Root cause

Each sta_->deleteInstance(leaf) triggers, per pin disconnect, the
callback chain dbStaCbk::inDbITermPreDisconnect
dbNetwork::disconnectPinBefore(pin) (src/dbSta/src/dbNetwork.cc:2782).

For driver pins, disconnectPinBefore previously called
dbNet::findRelatedModNets (src/odb/src/db/dbNet.cpp:2486) — an
unmemoized DFS through the modnet hierarchy graph allocating a fresh
std::set<dbModNet*> per call. For hierarchical pins it additionally
called dbModNet::findRelatedNet.

Why those DFS calls existed: the inherited driver cache
net_drvr_pin_map_ (declared in upstream OpenSTA src/sta/network/Network.cc
— must NOT be modified per CLAUDE.md rule #1) was keyed by raw Net*
pointer, accepting both dbNet* (flat) and dbModNet* (hier) keys. A
single physical net named by N modnets in the hierarchy had up to N
redundant entries with identical content (each populated by
findFlatDbNet(net)->getFirstDriverTerm()); evicting them all required
walking the modnet graph.

Per-disconnect work was O(reachable modnets); across an
eliminate_dead_logic sweep deleting M leaf instances with ~P pins each,
total cost was O(M × P × reachable_modnets). On the hierarchical asap7
design measured in #10327, this dominated synth_odb wall time.

Change

Canonicalize the cache key to flat dbNet*.

  • dbNetwork::drivers(const Net* net) translates any input net to its
    flat dbNet via findFlatDbNet() and uses that as the cache key.
    Modnet inputs converge on the same canonical entry; the cache holds
    at most one entry per flat net.
  • dbNetwork::disconnectPinBefore evicts one flat-net entry per pin —
    no DFS through the modnet hierarchy. The findRelatedModNets calls
    are removed.
  • findFlatDbNet memoizes the dbModNet → dbNet translation
    (modnet_to_flat_net_cache_); dbStaCbk invalidates it on the events
    that can change a modnet's flat-net mapping (modnet/modITerm/modBTerm
    create/destroy/connect/disconnect, plus dbNet destroy and flat
    iterm/bterm connects/disconnects that touch a modnet).

No new public API. No specialized "begin/end bulk delete" mode. Every
caller of sta::deleteInstance benefits, not just eliminateDeadLogic.

Why this is safe

  • All in-tree callers of drivers() consume the returned PinSet*
    immediately and do not rely on per-modnet cache identity (verified
    across rsz, dbSta, est, gui, drt, and upstream sta callers).
  • The PinSet content for a modnet was always equal to the flat net's
    content; sharing one entry produces identical observable behavior.
  • removeDriverFromCache(net, pin) (the two-arg form) erases a single
    Pin and composes correctly with multi-driver flat nets.
  • dbNet::findRelatedModNets itself is unchanged — sanity checks, dump,
    GUI, and dbInsertBuffer continue to use it.

Test plan

  • Existing eliminate_dead_logic{1,2}.tcl regressions pass.
  • Full `bazelisk test //src/...` (1547 targets) passes.

Performance

Measured on `asap7/swerv_wrapper` (`OPENROAD_HIERARCHICAL=1`,
`SYNTH_KEEP_MODULES` per design config), one run each:

Variant `eliminate_dead_logic` wall (ms) dead insts removed
`origin/master` baseline 2941 1264
#10327 (`dbsta-bulk-delete-mode`) 2959 1264
this PR 2888 1264

Honest caveat: at this scale (1264 dead instances, ~70× smaller than
the S size in #10327's reported benchmark) the modnet DFS is not the
dominant cost — even #10327's specialized bulk-delete path doesn't
move the needle here. The savings show up at larger scale (per #10327:
S=87k objs/753s → L=574k objs/1675s on a hierarchical design with many
`keep_hierarchy` boundaries). Could a maintainer with access to such a
workload re-time on this branch? The structural change should match
#10327's gains by construction (same DFS calls eliminated) without
adding the specialized API.

Out of scope

🤖 Generated with Claude Code

Resizer::eliminateDeadLogic was dominated on hierarchical designs by
dbNetwork::disconnectPinBefore -> dbNet::findRelatedModNets, an
unmemoized DFS through the modnet hierarchy fired on every pin
disconnect.

The DFS only existed to keep the inherited net_drvr_pin_map_ driver
cache in sync when a single physical net was named by multiple
dbModNets in the hierarchy: each modnet had its own cache entry, and
disconnecting a pin had to walk the full reachable component to evict
all of them. The cache content for every such entry was identical
(dbNetwork::drivers populated each from the flat net's
getFirstDriverTerm()), so the per-modnet entries were redundant.

Make the cache canonical: dbNetwork::drivers translates any input net
to its flat dbNet via findFlatDbNet() and keys the cache by that flat
dbNet only. disconnectPinBefore can then evict in O(1) -- just one
flat-net entry -- with no findRelatedModNets call. For hierarchical
pins (case 2) the modnet-to-flat-net translation is now memoized in
dbNetwork (modnet_to_flat_net_cache_), invalidated by dbStaCbk on the
events that can change the mapping (modnet/modITerm/modBTerm
create/destroy/connect/disconnect, plus flat iterm/bterm disconnects
that touch a modnet, plus dbNet destroy).

No new public API and no specialized "begin/end" mode: every caller of
sta::deleteInstance benefits, not just eliminateDeadLogic. Existing
rsz eliminate_dead_logic{1,2} tests pass unchanged; full //src/...
test suite (1547 targets) passes.

This supersedes the rejected approach in PR The-OpenROAD-Project#10327 and closes the
underlying complaint in issue The-OpenROAD-Project#10329.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@github-actions github-actions Bot added the size/M label May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

clang-tidy review says "All clean, LGTM! 👍"

@oharboe
Copy link
Copy Markdown
Collaborator Author

oharboe commented May 6, 2026

@povik I don't think it makes sense for me to drive this. I have no idea what this means.

@oharboe oharboe closed this May 6, 2026
Copy link
Copy Markdown
Contributor

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

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 memoization cache for mapping hierarchical nets to flat nets and refactors the driver cache to use canonical flat nets as keys, improving performance and simplifying invalidation logic. Feedback highlights a potential bug where disconnecting hierarchical pins might fail to clear stale driver cache entries and a thread-safety issue caused by using a static local variable initialized with an instance pointer.

Comment on lines +2808 to 2810
if (dbNet* db_net = findFlatDbNet(dbToSta(modnet))) {
removeDriverFromCache(dbToSta(db_net), pin);
}
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

When disconnecting a hierarchical pin (moditerm), removeDriverFromCache is called with the pin object representing the moditerm. However, the driver cache (net_drvr_pin_map_) stores Pin* objects representing flat driver pins (dbITerm or dbBTerm).

The Pin* for a dbModITerm is different from the Pin* for a dbITerm/dbBTerm, so the call to entry->second->erase(drvr) inside removeDriverFromCache will fail to find and remove the driver. This will leave stale entries in the cache.

A safer approach would be to invalidate the entire cache entry for the flat net, forcing it to be recomputed on the next access. This can be done by using the one-argument version of removeDriverFromCache.

    if (dbNet* db_net = findFlatDbNet(dbToSta(modnet))) {
      removeDriverFromCache(dbToSta(db_net));
    }

Comment on lines +5106 to +5107
static PinSet empty(this);
return &empty;
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

The use of a function-local static variable empty initialized with this can lead to incorrect behavior when multiple dbNetwork objects are used. The static PinSet is initialized only once, during the first call to this function path. Subsequent calls from different dbNetwork instances will receive a PinSet associated with the Network* of the first instance, not their own. This can cause subtle bugs or crashes.

To fix this, empty should be an instance member of dbNetwork rather than a static local variable.

For example, you could add a private member to dbNetwork in src/dbSta/include/db_sta/dbNetwork.hh:

class dbNetwork : public ConcreteNetwork
{
  // ...
private:
  // ...
  PinSet empty_pin_set_{this};
};

And then change this function to return a pointer to it:

if (db_net == nullptr) {
  return &empty_pin_set_;
}

@jhkim-pii
Copy link
Copy Markdown
Contributor

This PR removes flat nets only in the cache, and does not remove hier nets, which is incomplete.

@oharboe
Copy link
Copy Markdown
Collaborator Author

oharboe commented May 6, 2026

This PR removes flat nets only in the cache, and does not remove hier nets, which is incomplete.

Yes: out of my depth...

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.

Speed up synth_odb.tcl eliminate dead logic

2 participants