Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 170 additions & 20 deletions src/dpl/src/Place.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
#include <set>
#include <string>
#include <tuple>
#include <unordered_set>
#include <vector>

#include "PlacementDRC.h"
Expand Down Expand Up @@ -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

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.

The negotiation legalizer will be the default one soon. The diamond search will be kept only for fallback on some corner cases (preferably never used).

{
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<PQ_entry,
std::vector<PQ_entry>,
std::greater<PQ_entry>>
{
explicit ReusableHeap(std::vector<PQ_entry>&& backing)
: std::priority_queue<PQ_entry,
std::vector<PQ_entry>,
std::greater<PQ_entry>>(std::greater<PQ_entry>{},
std::move(backing))
{
}
std::vector<PQ_entry> takeBacking() { return std::move(this->c); }
};

// Generation-stamped replacement for the per-call
// `std::unordered_set<GridPt> 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<size_t>(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<size_t>(local_y) * width_ + local_x;
assert(local_x >= 0 && local_x < width_ && local_y >= 0
&& idx < stamps_.size());
return idx;
}

std::vector<uint32_t> 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<bgPoint>& queryBox)
{
Expand Down Expand Up @@ -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<PQ_entry, std::vector<PQ_entry>, std::greater<PQ_entry>>
positionsHeap;
std::unordered_set<GridPt> 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<PQ_entry> 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);
Comment on lines +1028 to +1031

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 Avoid spanning the visited bitmap to a distant center

When a group-assigned cell starts far outside its region and prePlaceGroups() cannot place it—such as when the region is full—placeGroups2() retries from the cell's original position while the search bounds are clamped to the group. This call allocates a dense rectangle spanning the distant center and the group in both dimensions, so a valid but badly displaced cell can request gigabytes or terabytes and terminate with std::bad_alloc; the previous hash set only stored the center and its immediate probes in this case. Size the bitmap to the reachable search window and track an out-of-window center separately.

Useful? React with 👍 / 👎.


// 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(
Expand All @@ -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
Expand All @@ -933,6 +1074,7 @@ PixelPt Opendp::diamondSearch(const Node* cell,
.sequence = sequence++});
}
}
heap_backing = positionsHeap.takeBacking();
return PixelPt();
}

Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down
24 changes: 20 additions & 4 deletions src/dpl/src/dbToOpendp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,27 @@ void Opendp::createNetwork()
///////////////////////////////////
using odb::dbInst;
auto block_insts = block->getInsts();
std::vector<dbInst*> 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<std::pair<std::string, dbInst*>> 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) {
Comment on lines +281 to +294

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

Since sorting by string names is highly expensive, we should prefer sorting by unique integer IDs (e.g., a->getId() < b->getId()) to achieve a deterministic order. This avoids all std::string allocations, auxiliary vectors, and expensive strcmp calls entirely.

  std::vector<dbInst*> insts(block_insts.begin(), block_insts.end());
  // 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, [](dbInst* a, dbInst* b) {
    return a->getId() < b->getId();
  });

  for (dbInst* inst : insts) {
References
  1. For tie-breaking in sort comparators, prefer using unique integer IDs (e.g., cell1->id() < cell2->id()) instead of string comparisons (e.g., cell1->name() < cell2->name()), as string comparisons are highly expensive.

// Skip instances which are not placeable.
if (!inst->getMaster()->isCoreAutoPlaceable()) {
continue;
Expand Down
11 changes: 11 additions & 0 deletions src/dpl/src/infrastructure/Objects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/dpl/src/infrastructure/Objects.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<MasterEdge> edges_;
Expand Down Expand Up @@ -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);

Expand Down
Loading
Loading