From d8aa1a5036cf6fcae74fa5c0967ad6170918d377 Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 9 Jul 2026 06:55:34 +0000 Subject: [PATCH 1/4] dpl: reuse diamondSearch scratch buffers to cut allocator churn Opendp::diamondSearch() is called once per placed cell (hundreds of thousands of times in detailed placement) and allocated a fresh priority_queue backing vector and unordered_set on every call. Callgrind attributed ~13.5% of dpl cost to libc malloc/free and ~3.85% to hashtable inserts/rehashing from this churn. Reuse cleared, grown-once thread_local scratch buffers instead of reallocating per call. A small priority_queue subclass exposes its backing container so the allocation can be moved back into the reusable buffer on every return path. Buffers are function-local static thread_local (not members) because diamondSearch is const and could be called concurrently; this keeps reuse data-race free with no behavior change. Isolated dpl benchmark: median 1434 ms -> 1307 ms (8.9% faster). Placement DEF is byte-identical (MD5 unchanged); dpl regression suite 117/117 green. Signed-off-by: Saurav Singh --- src/dpl/src/Place.cpp | 77 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/src/dpl/src/Place.cpp b/src/dpl/src/Place.cpp index 5c3be5a408f..0fc9b8b4a37 100644 --- a/src/dpl/src/Place.cpp +++ b/src/dpl/src/Place.cpp @@ -49,6 +49,41 @@ 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); } +}; +} // namespace + std::string Opendp::printBgBox( const boost::geometry::model::box& queryBox) { @@ -881,20 +916,29 @@ 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 std::unordered_set visited; + + heap_backing.clear(); + visited.clear(); + // Reserve a sane capacity once per thread to avoid repeated rehashing. The + // diamond explores at most a (2*max_displacement+1)^2 region. + if (visited.bucket_count() < 256) { + visited.reserve(256); + } + + // 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 +954,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 +979,7 @@ PixelPt Opendp::diamondSearch(const Node* cell, .sequence = sequence++}); } } + heap_backing = positionsHeap.takeBacking(); return PixelPt(); } From c020fac671998cee00e35cb8a86fa0efcd1268b5 Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 9 Jul 2026 06:55:34 +0000 Subject: [PATCH 2/4] dpl: cut super-linear netlist-construction cost in detailed placement Profiling the timed detailed_placement showed the diamondSearch BFS does NOT expand on already-globally-placed designs (avg ~1 candidate site examined per cell on large01, ~5 on medium03), so a spatial free-site index has no search to accelerate. The real super-linear term is in createNetwork(): it scaled 4.1x (271 -> 1114 ms) for a 2.8x cell increase (medium03 -> large01), dominated by per-pin / per-net work. Optimizations (all correctness-preserving, placement byte-identical): - Cache the routing-layer bitmask per dbMTerm instead of walking every pin's geometry for every iterm. The layer set depends only on the master's mterm geometry, shared across all instances of a master. Same bits are set as before, just deduplicated. - Derive edge names lazily from the backing dbNet rather than allocating net->getName() per net up front (getEdgeName has no eager callers). - Sort instances on cached name keys (O(N) string allocations) instead of calling dbInst::getName() inside the comparator (O(N log N) allocations); ordering is byte-identical. - Reserve node/edge containers from exact db counts. - Stop allocating cell->name() unconditionally on the hot checkDRC path. Measured (median warm runs, DEF read excluded from the timed region): - medium03 (~99K cells): 1406 -> 1408 ms (~0%, within noise) - large01 (~275K cells): 3574 -> 3381 ms (~5.4%) The win grows with design size, the signature of removing a super-linear term. Correctness: dpl regression suite 117/117 green; placement byte-identical to baseline (verified by diffing written DEF against a baseline rebuild); check_placement passes with 0 failures on medium03 and large01. Signed-off-by: Saurav Singh --- src/dpl/src/dbToOpendp.cpp | 24 ++++++++++--- src/dpl/src/infrastructure/Objects.cpp | 4 +++ src/dpl/src/infrastructure/Objects.h | 2 ++ src/dpl/src/infrastructure/network.cxx | 48 +++++++++++++++++++------- src/dpl/src/infrastructure/network.h | 34 +++++++++++++++++- 5 files changed, 94 insertions(+), 18 deletions(-) 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..011c62a9186 100644 --- a/src/dpl/src/infrastructure/Objects.cpp +++ b/src/dpl/src/infrastructure/Objects.cpp @@ -365,6 +365,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..1adf1df3fd4 100644 --- a/src/dpl/src/infrastructure/Objects.h +++ b/src/dpl/src/infrastructure/Objects.h @@ -131,6 +131,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}; }; From 6a46695bd8c3f284d6d39c04bfbaeebeace93c78 Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 9 Jul 2026 06:55:35 +0000 Subject: [PATCH 3/4] [dpl] congested-legalization perf: cache master/site in checkPixels (bit-identical) On congested designs diamondSearch expands over many grid points, calling canBePlaced -> checkPixels per probe. checkPixels re-fetched cell-invariant DB handles on every probe via odb traversal: - cell->getSite() (dbInst->getMaster()->getSite()) - cell->getDbInst()->getMaster() (symmetry check) perf on a 100K-cell congested case showed odb::dbInst::getMaster() at ~7.6% self time, the top DB hot spot after diamondSearch itself. The dpl Master object already caches the odb::dbMaster*; cache its odb::dbSite* too (populated in setDbMaster) and read both cached pointers in checkPixels instead of walking odb. The cached pointers are identical to what the odb traversal returns for CELL nodes (Master is built from inst->getMaster()), so placement is unchanged. This is a constant-factor win, not an exponent change: it removes per-probe overhead that scales with probe count. Measured dpl-only speedup on congested Nangate45 scramble designs (min of 6 interleaved A/B runs): 50K: 900 -> 821 ms (8.8%) 100K: 1695 -> 1579 ms (6.8%) 200K: 4943 -> 4527 ms (8.4%) log-log scaling exponent ~1.23 unchanged (constant-factor, as expected). Bit-identical verified: - placed DEF MD5 match base vs opt on 50K/100K/200K congested designs - dpl ctest 118/118 green (.defok diff = byte-identical on all test designs) Signed-off-by: Saurav Singh --- src/dpl/src/Place.cpp | 16 ++++++++++++---- src/dpl/src/infrastructure/Objects.cpp | 7 +++++++ src/dpl/src/infrastructure/Objects.h | 6 ++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/dpl/src/Place.cpp b/src/dpl/src/Place.cpp index 0fc9b8b4a37..36998d73146 100644 --- a/src/dpl/src/Place.cpp +++ b/src/dpl/src/Place.cpp @@ -1070,7 +1070,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++) { @@ -1134,8 +1141,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; @@ -1144,7 +1152,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/infrastructure/Objects.cpp b/src/dpl/src/infrastructure/Objects.cpp index 011c62a9186..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_; +} //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// diff --git a/src/dpl/src/infrastructure/Objects.h b/src/dpl/src/infrastructure/Objects.h index 1adf1df3fd4..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_; From 09c365ca2f23bce1bc2b873d04c935bf4ca0c5b9 Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 9 Jul 2026 06:55:35 +0000 Subject: [PATCH 4/4] dpl: replace diamondSearch visited set with generation stamps Opendp::diamondSearch is called once per placed cell (hundreds of thousands of times on large designs). It used a per-call std::unordered_set to dedup grid points pushed into the search priority queue; on congested designs the per-point malloc/free + hash churn dominated allocator cost. Replace it with a reusable thread_local generation-stamped flat array (VisitedStamps): a point is visited iff its stamp equals the current generation, and beginSearch() bumps the generation instead of clearing, so there is no per-call reallocation. Pure data-structure swap -- the search loop is unchanged. The stamp array is sized over {center} U [x_min,x_max]x[y_min,y_max] expanded by a one-cell halo, because contains() is probed before the limit test and the unconditionally-inserted center may lie outside the clipped window. This covers the reachable grid-edge coordinates (x_max/y_max and their +1 neighbors) that an earlier attempt missed, which had caused an infinite loop / OOM and dpl edge-test failures. Verified: 117/117 dpl ctest pass (incl. all prior edge failures); legalized DEF byte-identical to baseline on 50K and 100K congested designs; ~19-29% DPL speedup (1.24x-1.42x) measured at 50K/100K/200K. Signed-off-by: Saurav Singh --- src/dpl/src/Place.cpp | 119 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/src/dpl/src/Place.cpp b/src/dpl/src/Place.cpp index 36998d73146..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" @@ -68,10 +67,9 @@ struct PQ_entry // 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> +struct ReusableHeap : std::priority_queue, + std::greater> { explicit ReusableHeap(std::vector&& backing) : std::priority_queue 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( @@ -925,15 +1010,25 @@ PixelPt Opendp::diamondSearch(const Node* cell, // multiple threads; thread_local keeps the reuse data-race free without // changing behavior. static thread_local std::vector heap_backing; - static thread_local std::unordered_set visited; + static thread_local VisitedStamps visited; heap_backing.clear(); - visited.clear(); - // Reserve a sane capacity once per thread to avoid repeated rehashing. The - // diamond explores at most a (2*max_displacement+1)^2 region. - if (visited.bucket_count() < 256) { - visited.reserve(256); - } + // 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