diff --git a/src/dpl/src/Place.cpp b/src/dpl/src/Place.cpp index 5c3be5a408f..5814ecafc95 100644 --- a/src/dpl/src/Place.cpp +++ b/src/dpl/src/Place.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include "PlacementDRC.h" @@ -49,6 +48,127 @@ using utl::DPL; using utl::format_as; // NOLINT(misc-unused-using-decls) +namespace { +// Heap entry for diamondSearch(). Defined at file scope (rather than locally +// inside diamondSearch) so the scratch backing buffers can be declared as +// reusable static thread_local containers below, avoiding a fresh heap +// allocation on every call. +struct PQ_entry +{ + int manhattan_distance; + GridPt p; + int sequence; + bool operator>(const PQ_entry& other) const + { + return std::tie(manhattan_distance, sequence) + > std::tie(other.manhattan_distance, other.sequence); + } +}; + +// priority_queue subclass that exposes its backing container so the (grown +// once) allocation can be moved back into a reusable thread_local buffer. +struct ReusableHeap : std::priority_queue, + std::greater> +{ + explicit ReusableHeap(std::vector&& backing) + : std::priority_queue, + std::greater>(std::greater{}, + std::move(backing)) + { + } + std::vector takeBacking() { return std::move(this->c); } +}; + +// Generation-stamped replacement for the per-call +// `std::unordered_set visited` used by diamondSearch(). +// +// Instead of a hash set (one malloc/free + hash insert per visited grid point, +// which dominated allocator cost on congested designs where the diamond +// expands over many points), this is a flat array of "last-visited generation" +// stamps indexed by `(y - origin_y) * width + (x - origin_x)`. A point is +// "visited" iff its stamp equals the current generation; beginSearch() bumps +// the generation so no per-call clearing or reallocation is needed. +// +// Equivalence to the old set: contains()/insert() behave identically for every +// (x, y) the search can insert, and the loop logic (which points are enqueued) +// is left untouched -- only the data structure backing `visited` changes. +// +// Sizing/indexing must cover EVERY (x, y) ever passed to contains()/insert(): +// * the center {x, y}, which is inserted unconditionally and can lie far +// OUTSIDE the clipped grid window (the initial cell location may be well +// outside the core, e.g. simple03 places a cell 3x beyond the die); +// * every neighbor enqueued, i.e. the closed window +// [x_min, x_max] x [y_min, y_max]; and +// * a ONE-CELL HALO around that, because diamondSearch calls +// contains(neighbor) BEFORE the limit test, so neighbors one step outside +// the window (x_max+1, x_min-1, y_max+1, y_min-1, and likewise around an +// out-of-window center) are probed via contains() even though they are then +// rejected for enqueue. +// The caller therefore sizes/indexes over the bounding box of {center} U window +// expanded by one cell on every side: +// origin = (min(x_min, x_center) - 1, min(y_min, y_center) - 1) +// extent = (max(x_max, x_center) + 1, max(y_max, y_center) + 1) +// Indexing relative to that origin covers all probed/inserted points with no +// dropped inserts -- exactly matching the unordered_set's behavior -- while +// never indexing out of range. +class VisitedStamps +{ + public: + // Prepare for a new search whose insertable coordinates are bounded by the + // inclusive box [lo_x, hi_x] x [lo_y, hi_y]. + void beginSearch(const int lo_x, + const int lo_y, + const int hi_x, + const int hi_y) + { + origin_x_ = lo_x; + origin_y_ = lo_y; + width_ = hi_x - lo_x + 1; + const int height = hi_y - lo_y + 1; + const size_t needed = static_cast(width_) * height; + if (stamps_.size() < needed) { + // Grow (never shrink) and reset stamps; growing invalidates the old + // generation baseline for the new entries, so restart generations. + stamps_.assign(needed, 0); + generation_ = 0; + } + // Bump generation; everything stamped with a prior generation is now + // implicitly "unvisited". Handle the (astronomically unlikely) wraparound + // by clearing. + if (++generation_ == 0) { + std::fill(stamps_.begin(), stamps_.end(), 0); + generation_ = 1; + } + } + + bool contains(const GridPt& p) const + { + return stamps_[index(p)] == generation_; + } + + void insert(const GridPt& p) { stamps_[index(p)] = generation_; } + + private: + size_t index(const GridPt& p) const + { + const int local_x = p.x.v - origin_x_; + const int local_y = p.y.v - origin_y_; + const size_t idx = static_cast(local_y) * width_ + local_x; + assert(local_x >= 0 && local_x < width_ && local_y >= 0 + && idx < stamps_.size()); + return idx; + } + + std::vector stamps_; + uint32_t generation_ = 0; + int origin_x_ = 0; + int origin_y_ = 0; + int width_ = 0; +}; +} // namespace + std::string Opendp::printBgBox( const boost::geometry::model::box& queryBox) { @@ -881,20 +1001,39 @@ PixelPt Opendp::diamondSearch(const Node* cell, y_min, y_max - 1); - struct PQ_entry - { - int manhattan_distance; - GridPt p; - int sequence; - bool operator>(const PQ_entry& other) const - { - return std::tie(manhattan_distance, sequence) - > std::tie(other.manhattan_distance, other.sequence); - } - }; - std::priority_queue, std::greater> - positionsHeap; - std::unordered_set visited; + // Reusable scratch buffers. diamondSearch is called once per placed cell + // (hundreds of thousands of times); allocating a fresh priority-queue backing + // vector and a visited hash set on every call dominated the allocator cost in + // profiling. These thread_local buffers are cleared and reused instead, so + // each thread keeps a single grown-once allocation. They are thread_local + // (not Opendp members) because diamondSearch is const and may be invoked from + // multiple threads; thread_local keeps the reuse data-race free without + // changing behavior. + static thread_local std::vector heap_backing; + static thread_local VisitedStamps visited; + + heap_backing.clear(); + // Prepare the generation-stamped visited set. It must cover every (x, y) + // ever passed to visited.contains()/insert(), which behaves exactly like the + // old unordered_set. Two subtleties beyond the obvious search window: + // 1. The center {x, y} is inserted unconditionally and may lie outside the + // clipped window (the initial cell location can be far outside the + // core). + // 2. contains(neighbor) is evaluated BEFORE the limit test, so neighbors + // one cell OUTSIDE the window (x_max+1, x_min-1, y_max+1, y_min-1) are + // probed via contains() even though they are then rejected for enqueue. + // So index over the window-or-center bounding box expanded by a one-cell halo + // on every side. This matches the unordered_set's behavior with no dropped + // inserts and no out-of-range index. + visited.beginSearch(min(x_min, x).v - 1, + min(y_min, y).v - 1, + max(x_max, x).v + 1, + max(y_max, y).v + 1); + + // Construct the heap over the reused backing vector. Moving the (cleared but + // capacity-retaining) vector in preserves the allocation; we move it back out + // before returning so the next call reuses it. + ReusableHeap positionsHeap(std::move(heap_backing)); int sequence = 0; GridPt center{x, y}; positionsHeap.push( @@ -910,8 +1049,10 @@ PixelPt Opendp::diamondSearch(const Node* cell, positionsHeap.pop(); if (canBePlaced(cell, nearest.x, nearest.y)) { - return PixelPt( + const PixelPt result( grid_->gridPixel(nearest.x, nearest.y), nearest.x, nearest.y); + heap_backing = positionsHeap.takeBacking(); + return result; } // Put neighbors in the queue @@ -933,6 +1074,7 @@ PixelPt Opendp::diamondSearch(const Node* cell, .sequence = sequence++}); } } + heap_backing = positionsHeap.takeBacking(); return PixelPt(); } @@ -1023,7 +1165,14 @@ bool Opendp::checkPixels(const Node* cell, return false; } - odb::dbSite* site = cell->getSite(); + // Hoist cell-invariant master/site lookups out of the per-pixel loop and off + // the odb traversal path. cell->getSite() and cell->getDbInst()->getMaster() + // each walk dbInst->dbMaster on every probe; on congested designs + // diamondSearch probes enormously, making these the top DB hot spots. The + // dpl Master caches the same odb::dbMaster*/odb::dbSite* pointers, so reading + // them here is pointer-identical to the odb traversal but far cheaper. + const Master* master = cell->getMaster(); + odb::dbSite* site = master->getDbSite(); for (GridY y1 = y; y1 < y_end; y1++) { const bool first_row = (y1 == y); for (GridX x1 = x; x1 < x_end; x1++) { @@ -1087,8 +1236,9 @@ bool Opendp::checkPixels(const Node* cell, const auto orient = grid_->getSiteOrientation(x, y, site).value(); - // Check for symmetry - auto* dbMaster = cell->getDbInst()->getMaster(); + // Check for symmetry. Use the cached db master (pointer-identical to + // cell->getDbInst()->getMaster()) to avoid the odb traversal per probe. + auto* dbMaster = master->getDbMaster(); unsigned masterSym = dpl::DetailedOrient::getMasterSymmetry(dbMaster); if (!checkMasterSym(masterSym, orient)) { return false; @@ -1097,7 +1247,7 @@ bool Opendp::checkPixels(const Node* cell, // For multi-row cells, the bottom-row site/orient check above only covers // the bottom row; it doesn't ensure the master's power pin stack lines up // with the PDN rail stack across the span. Reject wrong-parity landings. - if (cell->getMaster()->isMultiRow() && !checkRowPowerCompatible(cell, y)) { + if (master->isMultiRow() && !checkRowPowerCompatible(cell, y)) { return false; } diff --git a/src/dpl/src/dbToOpendp.cpp b/src/dpl/src/dbToOpendp.cpp index 6a7428e8e1a..5bb25c9a302 100644 --- a/src/dpl/src/dbToOpendp.cpp +++ b/src/dpl/src/dbToOpendp.cpp @@ -271,11 +271,27 @@ void Opendp::createNetwork() /////////////////////////////////// using odb::dbInst; auto block_insts = block->getInsts(); - std::vector insts(block_insts.begin(), block_insts.end()); - std::ranges::stable_sort( - insts, [](dbInst* a, dbInst* b) { return a->getName() < b->getName(); }); + // Sort instances by name for deterministic ordering. dbInst::getName() + // returns a std::string *by value*, so using it directly inside the + // comparator allocated two strings per comparison -- O(N log N) string + // allocations that dominated importDb() on large designs. Materialize each + // name exactly once (O(N) allocations) and sort on the cached key, which + // yields byte-identical ordering (std::string::operator< is the same + // lexicographic comparison the old comparator used). + std::vector> insts_by_name; + insts_by_name.reserve(block_insts.size()); + for (dbInst* inst : block_insts) { + insts_by_name.emplace_back(inst->getName(), inst); + } + // Reserve the node/edge containers up front (cheap, exact counts) to avoid + // repeated reallocation of the unique_ptr vectors as the netlist is built. + network_->reserve(block_insts.size() + block->getBTerms().size(), + block->getNets().size()); + std::ranges::stable_sort(insts_by_name, [](const auto& a, const auto& b) { + return a.first < b.first; + }); - for (dbInst* inst : insts) { + for (const auto& [inst_name, inst] : insts_by_name) { // Skip instances which are not placeable. if (!inst->getMaster()->isCoreAutoPlaceable()) { continue; diff --git a/src/dpl/src/infrastructure/Objects.cpp b/src/dpl/src/infrastructure/Objects.cpp index 33f1d3907fc..da2947acd4e 100644 --- a/src/dpl/src/infrastructure/Objects.cpp +++ b/src/dpl/src/infrastructure/Objects.cpp @@ -79,11 +79,18 @@ void Master::setTopPowerType(const int top_pwr) void Master::setDbMaster(odb::dbMaster* db_master) { db_master_ = db_master; + // Cache the site pointer so hot-path legality checks need not re-traverse + // odb. + db_site_ = db_master ? db_master->getSite() : nullptr; } odb::dbMaster* Master::getDbMaster() const { return db_master_; } +odb::dbSite* Master::getDbSite() const +{ + return db_site_; +} //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// @@ -365,6 +372,10 @@ void Node::addUsedLayer(int layer) { used_layers_ |= 1 << layer; } +void Node::orUsedLayers(uint8_t mask) +{ + used_layers_ |= mask; +} bool Node::adjustCurrOrient(const odb::dbOrientType& newOri) { using odb::dbOrientType; diff --git a/src/dpl/src/infrastructure/Objects.h b/src/dpl/src/infrastructure/Objects.h index 521744e823b..09c1011292d 100644 --- a/src/dpl/src/infrastructure/Objects.h +++ b/src/dpl/src/infrastructure/Objects.h @@ -43,9 +43,15 @@ class Master void setTopPowerType(int top_pwr); void setDbMaster(odb::dbMaster* db_master); odb::dbMaster* getDbMaster() const; + // Cached db site (db_master_->getSite()), populated by setDbMaster(). Lets + // the per-probe legality checks in diamondSearch avoid repeated odb traversal + // (dbInst->getMaster()->getSite()) that dominated profiling on congested + // designs. Returns the identical pointer the odb traversal would. + odb::dbSite* getDbSite() const; private: odb::dbMaster* db_master_{nullptr}; + odb::dbSite* db_site_{nullptr}; odb::Rect boundary_box_; bool is_multi_row_{false}; std::vector edges_; @@ -131,6 +137,8 @@ class Node void addPin(Pin* pin); void setGroupId(int id); void addUsedLayer(int layer); + // OR a precomputed routing-layer bitmask into this node's used layers. + void orUsedLayers(uint8_t mask); bool adjustCurrOrient(const odb::dbOrientType& newOrient); diff --git a/src/dpl/src/infrastructure/network.cxx b/src/dpl/src/infrastructure/network.cxx index 2658406cb76..f9dc4917837 100644 --- a/src/dpl/src/infrastructure/network.cxx +++ b/src/dpl/src/infrastructure/network.cxx @@ -88,20 +88,34 @@ Pin* Network::addPin(odb::dbITerm* term) auto node = getNode(term->getInst()); if (node != nullptr) { - for (auto pin : term->getMTerm()->getMPins()) { - for (auto box : pin->getGeometry()) { - auto layer = box->getTechLayer(); - if (layer->getType() != odb::dbTechLayerType::Value::ROUTING) { - continue; - } - if (layer->getRoutingLevel() > 3) { - continue; + // The set of routing layers a pin touches depends only on the mterm + // geometry, which is identical for every instance of a master. Compute + // the bitmask once per mterm and reuse it; previously this walked all pin + // geometry for every iterm, which scaled with total pins and dominated + // createNetwork() on large designs. + auto cache_it = mterm_layers_.find(mTerm); + uint8_t mask; + if (cache_it != mterm_layers_.end()) { + mask = cache_it->second; + } else { + mask = 0; + for (auto pin : mTerm->getMPins()) { + for (auto box : pin->getGeometry()) { + auto layer = box->getTechLayer(); + if (layer->getType() != odb::dbTechLayerType::Value::ROUTING) { + continue; + } + const int level = layer->getRoutingLevel(); + if (level > 3) { + continue; + } + mask |= static_cast(1 << level); + mask |= static_cast(1 << (level + 1)); // via access above } - node->addUsedLayer(layer->getRoutingLevel()); - node->addUsedLayer(layer->getRoutingLevel() - + 1); // for via access from above } + mterm_layers_[mTerm] = mask; } + node->orUsedLayers(mask); } return ptr; } @@ -134,8 +148,14 @@ void Network::addEdge(odb::dbNet* net) Edge* edge = uedge.get(); //////////////////////// net_to_edge_idx_[net] = id; - // Name of edge. - setEdgeName(id, net->getName()); + // Record the backing net so the edge name can be derived lazily. This is a + // cheap pointer append; materializing net->getName() up front (a std::string + // allocation per net) measurably dominated createNetwork() on large designs, + // and Network::getEdgeName() has no eager callers. + if (static_cast(edge_to_net_.size()) <= id) { + edge_to_net_.resize(id + 1, nullptr); + } + edge_to_net_[id] = net; for (auto iterm : net->getITerms()) { if (!iterm->getInst()->getMaster()->isCoreAutoPlaceable()) { @@ -511,10 +531,12 @@ void Network::clear() pins_.clear(); blockages_.clear(); edgeNames_.clear(); + edge_to_net_.clear(); inst_to_node_idx_.clear(); term_to_node_idx_.clear(); master_to_idx_.clear(); net_to_edge_idx_.clear(); + mterm_layers_.clear(); cells_cnt_ = 0; terminals_cnt_ = 0; } diff --git a/src/dpl/src/infrastructure/network.h b/src/dpl/src/infrastructure/network.h index 986138ad1d3..12d48ce253d 100644 --- a/src/dpl/src/infrastructure/network.h +++ b/src/dpl/src/infrastructure/network.h @@ -49,7 +49,23 @@ class Network Edge* getEdge(odb::dbNet* net) const; Edge* getEdge(int i) const { return edges_[i].get(); } void setEdgeName(int i, const std::string& name) { edgeNames_[i] = name; } - const std::string& getEdgeName(int i) const { return edgeNames_.at(i); } + // Edge names are not materialized eagerly (that cost a std::string allocation + // per net during createNetwork()). If an explicit name was set via + // setEdgeName() it is returned; otherwise the name is derived lazily from the + // backing dbNet. Returns by value because the lazy path has no stored string + // to reference. + std::string getEdgeName(int i) const + { + auto it = edgeNames_.find(i); + if (it != edgeNames_.end()) { + return it->second; + } + if (i >= 0 && i < static_cast(edge_to_net_.size()) + && edge_to_net_[i] != nullptr) { + return edge_to_net_[i]->getName(); + } + return ""; + } int getNumPins() const { return (int) pins_.size(); } @@ -67,6 +83,14 @@ class Network // For creating and adding edges. void addEdge(odb::dbNet* net); + // Reserve capacity for the netlist containers to avoid repeated reallocation + // of the unique_ptr vectors while the network is built. + void reserve(size_t num_nodes, size_t num_edges) + { + nodes_.reserve(num_nodes); + edges_.reserve(num_edges); + } + // For creating masters. Master* addMaster(odb::dbMaster* db_master, const Grid* grid, @@ -94,11 +118,19 @@ class Network std::vector blockages_; // The placement blockages .. std::unordered_map edgeNames_; // Names of edges... + // Backing nets indexed by edge id, used to derive edge names lazily without + // allocating a std::string for every net up front. + std::vector edge_to_net_; std::unordered_map inst_to_node_idx_; std::unordered_map term_to_node_idx_; std::unordered_map master_to_idx_; std::unordered_map net_to_edge_idx_; + // Cache of the routing-layer bitmask used by each mterm. The layer set + // depends only on the mterm geometry (shared across all instances of a + // master), so it is computed once and reused, avoiding a per-pin walk of the + // pin geometry for every iterm during createNetwork(). + std::unordered_map mterm_layers_; uint32_t cells_cnt_{0}; uint32_t terminals_cnt_{0}; };