2026-07-31 15:03:26 +01:00
|
|
|
// -*- mode: C++; c-file-style: "cc-mode" -*-
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
// DESCRIPTION: Verilator: Multi-threaded MTask graph contraction (coarsening)
|
|
|
|
|
//
|
|
|
|
|
// Code available from: https://verilator.org
|
|
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
//
|
|
|
|
|
// This program is free software; you can redistribute it and/or modify it
|
|
|
|
|
// under the terms of either the GNU Lesser General Public License Version 3
|
|
|
|
|
// or the Perl Artistic License Version 2.0.
|
|
|
|
|
// SPDX-FileCopyrightText: 2003-2026 Wilson Snyder
|
|
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
|
|
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
//
|
2026-08-17 23:14:52 +02:00
|
|
|
// Coarsens the fine-grained MTask graph by repeatedly contracting MTasks,
|
|
|
|
|
// merging along an edge, or merging two "sibling" MTasks until a
|
|
|
|
|
// critical-path limit is reached.
|
2026-07-31 15:03:26 +01:00
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
|
|
|
|
|
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
|
|
|
|
|
|
|
|
|
|
#include "V3Global.h"
|
|
|
|
|
#include "V3Graph.h"
|
|
|
|
|
#include "V3OrderMTaskGraph.h"
|
|
|
|
|
#include "V3PairingHeap.h"
|
2026-08-17 23:14:52 +02:00
|
|
|
#include "V3PoolAllocator.h"
|
2026-07-31 15:03:26 +01:00
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <limits>
|
|
|
|
|
#include <memory>
|
|
|
|
|
#include <type_traits>
|
|
|
|
|
#include <unordered_set>
|
|
|
|
|
#include <utility>
|
|
|
|
|
|
|
|
|
|
VL_DEFINE_DEBUG_FUNCTIONS;
|
|
|
|
|
|
|
|
|
|
class MergeCandidate;
|
|
|
|
|
class SiblingMC;
|
|
|
|
|
class EdgeMC;
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
//######################################################################
|
|
|
|
|
// Tunable settings
|
2026-07-31 15:03:26 +01:00
|
|
|
|
|
|
|
|
// Arbitrarily limit the number of edges on a single vertex that will be
|
|
|
|
|
// considered when enumerating siblings, to the given value. This protects
|
2026-08-17 23:14:52 +02:00
|
|
|
// the runtime in the presence of huge vertices.
|
2026-07-31 15:03:26 +01:00
|
|
|
//
|
2026-08-17 23:14:52 +02:00
|
|
|
// The sibling-merge is less important than the edge merge. Sibling merges
|
|
|
|
|
// can be disabled and result in halfway decent coarsening. Edge merges
|
|
|
|
|
// cannot be disabled, those are fundamental to the process. So, skipping
|
|
|
|
|
// the enumeration of some siblings on a few vertices does not have a large
|
|
|
|
|
// impact on the result of the partitioner.
|
2026-07-31 15:03:26 +01:00
|
|
|
//
|
2026-08-17 23:14:52 +02:00
|
|
|
// If vertices are small, the limit (at 26) approaches a no-op. Hence
|
|
|
|
|
// there's basically no cost to applying this limit even when no huge
|
|
|
|
|
// vertices are expected.
|
2026-07-31 15:03:26 +01:00
|
|
|
//
|
2026-08-17 23:14:52 +02:00
|
|
|
// If runtime is not a concern and the most precise result is desired,
|
|
|
|
|
// set the limit very high.
|
2026-07-31 15:03:26 +01:00
|
|
|
constexpr unsigned PART_SIBLING_EDGE_LIMIT = 26;
|
|
|
|
|
|
|
|
|
|
// If the user doesn't give one with '--threads-max-mtasks', we'll set the
|
2026-08-17 23:14:52 +02:00
|
|
|
// maximum # of MTasks to (# of threads * PART_DEFAULT_MAX_MTASKS_PER_THREAD)
|
2026-07-31 15:03:26 +01:00
|
|
|
constexpr unsigned PART_DEFAULT_MAX_MTASKS_PER_THREAD = 50;
|
|
|
|
|
|
|
|
|
|
//######################################################################
|
|
|
|
|
// MTask utility classes
|
|
|
|
|
|
|
|
|
|
struct MergeCandidateKey final {
|
|
|
|
|
// Note: Structure layout chosen to minimize padding in PairingHeap<*>::Node
|
2026-08-17 23:14:52 +02:00
|
|
|
uint64_t m_id; // Unique ID part of the key
|
|
|
|
|
uint64_t m_score; // Score part of the key
|
2026-07-31 15:03:26 +01:00
|
|
|
bool operator<(const MergeCandidateKey& other) const {
|
|
|
|
|
// First by Score then by ID, but notice that we want minimums using a max-heap, so reverse
|
|
|
|
|
return m_score > other.m_score || (m_score == other.m_score && m_id > other.m_id);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// For efficiency, MergeCandidateScoreboard elements must derive from
|
|
|
|
|
// PairingHeap<MergeCandidateKey>::Node
|
|
|
|
|
using MergeCandidateHeapNode = PairingHeap<MergeCandidateKey>::Node;
|
|
|
|
|
|
|
|
|
|
// Information associated with scoreboarding a merge candidate
|
|
|
|
|
class MergeCandidate VL_NOT_FINAL : public MergeCandidateHeapNode {
|
|
|
|
|
// Only the known subclasses can create or delete one of these
|
|
|
|
|
friend class SiblingMC;
|
|
|
|
|
friend class EdgeMC;
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// This structure is extremely hot. To save 8 bytes by not having a virtual
|
|
|
|
|
// function table, we implement the few polymorphic methods over the two
|
|
|
|
|
// known subclasses explicitly, using a bit of the id to denote the actual
|
|
|
|
|
// subtype.
|
2026-07-31 15:03:26 +01:00
|
|
|
|
|
|
|
|
// By using the bottom bits for flags, we can still use < to compare IDs without masking.
|
|
|
|
|
// <63:1> Serial number for ordering, <0> subtype (SiblingMC)
|
|
|
|
|
static constexpr uint64_t IS_SIBLING_MASK = 1ULL << 0;
|
|
|
|
|
static constexpr uint64_t ID_INCREMENT = 1ULL << 1;
|
|
|
|
|
|
|
|
|
|
bool isSiblingMC() const { return m_key.m_id & IS_SIBLING_MASK; }
|
|
|
|
|
|
|
|
|
|
// CONSTRUCTORS
|
|
|
|
|
explicit MergeCandidate(bool isSiblingMC) {
|
|
|
|
|
static uint64_t s_serial = 0;
|
|
|
|
|
s_serial += ID_INCREMENT; // +ID_INCREMENT so doesn't set the special bottom bits
|
|
|
|
|
m_key.m_id = s_serial | (isSiblingMC * IS_SIBLING_MASK);
|
|
|
|
|
}
|
|
|
|
|
~MergeCandidate() = default;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
// METHODS
|
|
|
|
|
SiblingMC* toSiblingMC(); // Instead of cast<>/as<>
|
|
|
|
|
EdgeMC* toEdgeMC(); // Instead of cast<>/as<>
|
2026-08-17 23:14:52 +02:00
|
|
|
bool mergeWouldCreateCycle(OrderMTaskGraph& graph);
|
2026-07-31 15:03:26 +01:00
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// The current score of this candidate, which changes as the graph is contracted
|
|
|
|
|
inline uint64_t currentScore();
|
|
|
|
|
// The score this candidate was last given, which is its key in the scoreboard heap
|
2026-07-31 15:03:26 +01:00
|
|
|
uint64_t score() const { return m_key.m_score; }
|
2026-08-17 23:14:52 +02:00
|
|
|
// Set the score of this candidate to its current value. Only valid while it is not in a heap.
|
|
|
|
|
void updateScore() { m_key.m_score = currentScore(); }
|
2026-07-31 15:03:26 +01:00
|
|
|
|
|
|
|
|
static MergeCandidate* heapNodeToElem(MergeCandidateHeapNode* nodep) {
|
|
|
|
|
return static_cast<MergeCandidate*>(nodep);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static_assert(sizeof(MergeCandidate) == sizeof(MergeCandidateHeapNode),
|
|
|
|
|
"Should not have a vtable");
|
|
|
|
|
|
|
|
|
|
// A pair of associated LogicMTask's that are merge candidates for sibling
|
|
|
|
|
// contraction
|
|
|
|
|
class SiblingMC final : public MergeCandidate {
|
|
|
|
|
LogicMTask* const m_ap; // The higher ID MTask
|
|
|
|
|
LogicMTask* const m_bp; // The lower ID MTask
|
|
|
|
|
|
|
|
|
|
V3ListLinks<SiblingMC> m_aLinks; // List links to store instances of this class
|
|
|
|
|
V3ListLinks<SiblingMC> m_bLinks; // List links to store instances of this class
|
|
|
|
|
|
|
|
|
|
V3ListLinks<SiblingMC>& aLinks() { return m_aLinks; }
|
|
|
|
|
V3ListLinks<SiblingMC>& bLinks() { return m_bLinks; }
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
// List type to store instances of this class
|
|
|
|
|
using AList = V3List<SiblingMC, &SiblingMC::aLinks>;
|
|
|
|
|
using BList = V3List<SiblingMC, &SiblingMC::bLinks>;
|
|
|
|
|
|
|
|
|
|
// CONSTRUCTORS
|
|
|
|
|
SiblingMC(LogicMTask* ap, LogicMTask* bp);
|
|
|
|
|
~SiblingMC() = default;
|
|
|
|
|
|
|
|
|
|
// METHODS
|
|
|
|
|
void unlinkA();
|
|
|
|
|
void unlinkB();
|
|
|
|
|
|
|
|
|
|
LogicMTask* ap() const { return m_ap; }
|
|
|
|
|
LogicMTask* bp() const { return m_bp; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static_assert(!std::is_polymorphic<SiblingMC>::value, "Should not have a vtable");
|
|
|
|
|
|
|
|
|
|
// A merge candidate associated with an MTaskEdge (edge contraction candidate)
|
|
|
|
|
class EdgeMC final : public MergeCandidate {
|
|
|
|
|
MTaskEdge* const m_edgep; // The associated edge
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
// CONSTRUCTORS
|
|
|
|
|
explicit EdgeMC(MTaskEdge* edgep)
|
|
|
|
|
: MergeCandidate{/* isSiblingMC: */ false}
|
|
|
|
|
, m_edgep{edgep} {}
|
|
|
|
|
~EdgeMC() = default;
|
|
|
|
|
|
|
|
|
|
// METHODS
|
|
|
|
|
MTaskEdge* edgep() const { return m_edgep; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static_assert(!std::is_polymorphic<EdgeMC>::value, "Should not have a vtable");
|
|
|
|
|
|
|
|
|
|
// Auxiliary data associated with each LogicMTask during Contraction, attached via
|
|
|
|
|
// LogicMTask::userp(). Kept out of LogicMTask itself so that LogicMTask does not depend on the
|
|
|
|
|
// MergeCandidate hierarchy (which the SiblingMC lists reference).
|
|
|
|
|
struct MTaskContractionData final {
|
|
|
|
|
// MTasks for which a SiblingMC exists with the owning MTask as the higher ID MTask (m_ap)
|
|
|
|
|
std::unordered_set<LogicMTask*> siblings;
|
|
|
|
|
// SiblingMCs for which the owning MTask is the higher ID MTask (m_ap in SiblingMC)
|
|
|
|
|
SiblingMC::AList aSiblingMCs;
|
|
|
|
|
// SiblingMCs for which the owning MTask is the lower ID MTask (m_bp in SiblingMC)
|
|
|
|
|
SiblingMC::BList bSiblingMCs;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The MTaskContractionData attached to 'mtaskp' (see LogicMTask::userp)
|
|
|
|
|
static MTaskContractionData& mtaskData(const LogicMTask* mtaskp) {
|
|
|
|
|
return *static_cast<MTaskContractionData*>(mtaskp->userp());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The EdgeMC associated with 'edgep' while it is on the scoreboard, or nullptr otherwise. Held in
|
|
|
|
|
// the edge's user pointer (see MTaskEdge), kept here so MTaskEdge does not depend on EdgeMC.
|
|
|
|
|
static EdgeMC* edgeMC(const MTaskEdge* edgep) { return static_cast<EdgeMC*>(edgep->userp()); }
|
|
|
|
|
|
|
|
|
|
// Instead of dynamic cast
|
|
|
|
|
SiblingMC* MergeCandidate::toSiblingMC() {
|
|
|
|
|
return isSiblingMC() ? static_cast<SiblingMC*>(this) : nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EdgeMC* MergeCandidate::toEdgeMC() { return isSiblingMC() ? nullptr : static_cast<EdgeMC*>(this); }
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
bool MergeCandidate::mergeWouldCreateCycle(OrderMTaskGraph& graph) {
|
|
|
|
|
// Sibling merge: merging creates a cycle if either sibling is reachable from the other
|
|
|
|
|
if (const SiblingMC* const sibp = toSiblingMC()) {
|
|
|
|
|
return graph.pathExists(sibp->ap(), sibp->bp(), nullptr)
|
|
|
|
|
|| graph.pathExists(sibp->bp(), sibp->ap(), nullptr);
|
|
|
|
|
}
|
2026-07-31 15:03:26 +01:00
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Edge merge: merging creates a cycle if there is another path between the two MTasks
|
|
|
|
|
MTaskEdge* const edgep = toEdgeMC()->edgep();
|
|
|
|
|
return graph.pathExists(edgep->fromMTaskp(), edgep->toMTaskp(), edgep);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
uint64_t MergeCandidate::currentScore() {
|
|
|
|
|
// Score this candidate. The score is the new local CP length if we merge this candidate.
|
|
|
|
|
// ("Local" means the longest critical path running through the merged node.)
|
2026-07-31 15:03:26 +01:00
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Sibling merge
|
2026-07-31 15:03:26 +01:00
|
|
|
if (const SiblingMC* const sibp = toSiblingMC()) {
|
2026-08-17 23:14:52 +02:00
|
|
|
const LogicMTask* const ap = sibp->ap();
|
|
|
|
|
const LogicMTask* const bp = sibp->bp();
|
|
|
|
|
const uint64_t mergedCpFwd
|
|
|
|
|
= std::max(ap->cpExclusive<GraphWay::FORWARD>(), bp->cpExclusive<GraphWay::FORWARD>());
|
|
|
|
|
const uint64_t mergedCpRev
|
|
|
|
|
= std::max(ap->cpExclusive<GraphWay::REVERSE>(), bp->cpExclusive<GraphWay::REVERSE>());
|
|
|
|
|
return mergedCpRev + mergedCpFwd + ap->cost() + bp->cost();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Edge merge
|
|
|
|
|
{
|
|
|
|
|
MTaskEdge* const edgep = toEdgeMC()->edgep();
|
|
|
|
|
const LogicMTask* const fromp = edgep->fromMTaskp();
|
|
|
|
|
const LogicMTask* const top = edgep->toMTaskp();
|
|
|
|
|
const uint64_t mergedCpFwd = std::max(fromp->cpExclusive<GraphWay::FORWARD>(),
|
|
|
|
|
top->cpExclusiveWithout<GraphWay::FORWARD>(edgep));
|
|
|
|
|
const uint64_t mergedCpRev = std::max(fromp->cpExclusiveWithout<GraphWay::REVERSE>(edgep),
|
|
|
|
|
top->cpExclusive<GraphWay::REVERSE>());
|
2026-07-31 15:03:26 +01:00
|
|
|
// Give a slight preference to sibling merges by increasing the cost of edge merges.
|
|
|
|
|
// This biases towards sibling merges in case they are equal score with edge merges.
|
|
|
|
|
// This avoid a central node growing while many leaves remain due to edge merges.
|
2026-08-17 23:14:52 +02:00
|
|
|
return 1 + mergedCpRev + mergedCpFwd + fromp->cost() + top->cost();
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SiblingMC::SiblingMC(LogicMTask* ap, LogicMTask* bp)
|
|
|
|
|
: MergeCandidate{/* isSiblingMC: */ true}
|
|
|
|
|
, m_ap{ap}
|
|
|
|
|
, m_bp{bp} {
|
|
|
|
|
// Storage management depends on this
|
|
|
|
|
UASSERT(ap->id() > bp->id(), "Should be ordered");
|
|
|
|
|
UDEBUGONLY(UASSERT(mtaskData(ap).siblings.count(bp), "Should be in sibling map"););
|
|
|
|
|
mtaskData(m_ap).aSiblingMCs.linkBack(this);
|
|
|
|
|
mtaskData(m_bp).bSiblingMCs.linkBack(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SiblingMC::unlinkA() {
|
|
|
|
|
VL_ATTR_UNUSED const size_t removed = mtaskData(m_ap).siblings.erase(m_bp);
|
|
|
|
|
UDEBUGONLY(UASSERT(removed == 1, "Should have been in sibling set"););
|
|
|
|
|
mtaskData(m_ap).aSiblingMCs.unlink(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SiblingMC::unlinkB() { mtaskData(m_bp).bSiblingMCs.unlink(this); }
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Scoreboard of MTask merge candidates.
|
2026-07-31 15:03:26 +01:00
|
|
|
//
|
2026-08-17 23:14:52 +02:00
|
|
|
// This is a heap, sorted by the local critical path that would result from merging the candidate,
|
|
|
|
|
// that is: the longest critical path running through the merged MTask. Merges proceed by picking
|
|
|
|
|
// the candidate yielding the lowest such critical path, which is the merge that does the least
|
|
|
|
|
// damage: a merge can only ever lengthen paths, and one whose local critical path is no longer
|
|
|
|
|
// than the current global critical path cannot lengthen that at all.
|
|
|
|
|
//
|
|
|
|
|
// A candidate's score changes as the graph is contracted, so the score a candidate is in the heap
|
|
|
|
|
// with is only the score it was last given, which is a lower bound on its current score. This
|
|
|
|
|
// makes the top of the heap a lower bound on the score of every candidate, so the caller can pick
|
|
|
|
|
// the best candidate by checking whether the top still has the score it was given, and
|
|
|
|
|
// rescoring it if not (see the contraction loop).
|
|
|
|
|
//
|
|
|
|
|
// The scoreboard owns the lifetime of the merge candidate objects: callers add/remove candidates
|
|
|
|
|
// via the methods below and never allocate or free them directly. For edges this maintains the
|
|
|
|
|
// invariant that an MTaskEdge has an associated EdgeMC (held in its userp()), if and only if it is
|
|
|
|
|
// currently on the scoreboard.
|
2026-07-31 15:03:26 +01:00
|
|
|
class MergeCandidateScoreboard final {
|
|
|
|
|
// TYPES
|
|
|
|
|
using Heap = PairingHeap<MergeCandidateKey>;
|
|
|
|
|
|
|
|
|
|
// MEMBERS
|
2026-08-17 23:14:52 +02:00
|
|
|
Heap m_heap; // The heap of candidates, keyed on their score (see class comment above)
|
|
|
|
|
PoolAllocator<EdgeMC> m_edgeMCPool; // Allocator for the edge merge candidates
|
|
|
|
|
PoolAllocator<SiblingMC> m_siblingMCPool; // Allocator for the sibling merge candidates
|
2026-07-31 15:03:26 +01:00
|
|
|
|
|
|
|
|
// METHODS
|
2026-08-17 23:14:52 +02:00
|
|
|
// Set the score of a candidate and add it to the heap. The score is computed from the critical
|
|
|
|
|
// paths of the MTasks, which are up to date while the graph is being contracted.
|
|
|
|
|
void insert(MergeCandidate* nodep) {
|
|
|
|
|
nodep->updateScore();
|
|
|
|
|
m_heap.insert(nodep);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove a candidate from the scoreboard.
|
2026-08-17 23:14:52 +02:00
|
|
|
void remove(MergeCandidate* nodep) { m_heap.remove(nodep); }
|
2026-07-31 15:03:26 +01:00
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
// CONSTRUCTORS
|
|
|
|
|
MergeCandidateScoreboard() = default;
|
|
|
|
|
~MergeCandidateScoreboard() = default;
|
|
|
|
|
VL_UNCOPYABLE(MergeCandidateScoreboard);
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// The candidate with the lowest score it was given, or nullptr if the scoreboard is empty.
|
|
|
|
|
// Note this is only a lower bound on the best current score, see the class comment above.
|
|
|
|
|
MergeCandidate* best() const { return MergeCandidate::heapNodeToElem(m_heap.max()); }
|
2026-07-31 15:03:26 +01:00
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Update the score of a candidate to its current value, and reposition it in the heap
|
|
|
|
|
void rescore(MergeCandidate* nodep) {
|
|
|
|
|
remove(nodep);
|
|
|
|
|
insert(nodep);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Create a merge candidate for 'edgep' and add it to the scoreboard
|
|
|
|
|
void addEdgeMC(MTaskEdge* edgep) {
|
2026-07-31 15:03:26 +01:00
|
|
|
UDEBUGONLY(UASSERT(!edgep->userp(), "Edge already has a merge candidate"););
|
2026-08-17 23:14:52 +02:00
|
|
|
EdgeMC* const edgeMCp = m_edgeMCPool.alloc(edgep);
|
2026-07-31 15:03:26 +01:00
|
|
|
edgep->userp(edgeMCp);
|
2026-08-17 23:14:52 +02:00
|
|
|
insert(edgeMCp);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
2026-08-17 23:14:52 +02:00
|
|
|
// Remove 'edgep's merge candidate from the scoreboard and release it
|
|
|
|
|
void removeEdgeMC(MTaskEdge* edgep) {
|
2026-07-31 15:03:26 +01:00
|
|
|
EdgeMC* const edgeMCp = edgeMC(edgep);
|
|
|
|
|
UDEBUGONLY(UASSERT(edgeMCp, "Edge has no merge candidate"););
|
|
|
|
|
edgep->userp(nullptr);
|
|
|
|
|
remove(edgeMCp);
|
2026-08-17 23:14:52 +02:00
|
|
|
VL_DO_DANGLING(m_edgeMCPool.free(edgeMCp), edgeMCp);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a sibling merge candidate for 'ap' and 'bp' and add it to the scoreboard
|
2026-08-17 23:14:52 +02:00
|
|
|
void addSiblingMC(LogicMTask* ap, LogicMTask* bp) { insert(m_siblingMCPool.alloc(ap, bp)); }
|
|
|
|
|
// Remove sibling merge candidate 'smcp' from the scoreboard and release it
|
|
|
|
|
void removeSiblingMC(SiblingMC* smcp) {
|
2026-07-31 15:03:26 +01:00
|
|
|
remove(smcp);
|
|
|
|
|
smcp->unlinkA();
|
|
|
|
|
smcp->unlinkB();
|
2026-08-17 23:14:52 +02:00
|
|
|
VL_DO_DANGLING(m_siblingMCPool.free(smcp), smcp);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Remove the given merge candidate, whichever kind it is, from the scoreboard and release it
|
|
|
|
|
void removeMC(MergeCandidate* nodep) {
|
|
|
|
|
if (SiblingMC* const smcp = nodep->toSiblingMC()) {
|
|
|
|
|
removeSiblingMC(smcp);
|
2026-07-31 15:03:26 +01:00
|
|
|
} else {
|
2026-08-17 23:14:52 +02:00
|
|
|
removeEdgeMC(nodep->toEdgeMC()->edgep());
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-08-17 23:14:52 +02:00
|
|
|
};
|
2026-07-31 15:03:26 +01:00
|
|
|
|
|
|
|
|
//######################################################################
|
2026-08-17 23:14:52 +02:00
|
|
|
// Contraction - Perform greedy edge or sibling merges on the MTask graph
|
2026-07-31 15:03:26 +01:00
|
|
|
class Contraction final {
|
|
|
|
|
// MEMBERS
|
|
|
|
|
OrderMTaskGraph& m_mTaskGraph; // The Mtask graph
|
2026-08-17 23:14:52 +02:00
|
|
|
uint64_t m_scoreLimit; // Critical path limit for merges
|
2026-07-31 15:03:26 +01:00
|
|
|
MergeCandidateScoreboard m_sb; // Scoreboard
|
|
|
|
|
// Auxiliary per-MTask data (the SiblingMC lists) attached to each MTask via its user pointer.
|
2026-08-17 23:14:52 +02:00
|
|
|
// Owned here for the lifetime of this Contraction. A single array, as the number of MTasks
|
|
|
|
|
// can only decrease during contraction.
|
2026-07-31 15:03:26 +01:00
|
|
|
std::unique_ptr<MTaskContractionData[]> m_mtaskDatap;
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Add merge candidates for all edges of 'mtaskp' to the scoreboard
|
|
|
|
|
void addEdgeMCs(LogicMTask* mtaskp) {
|
|
|
|
|
for (V3GraphEdge& e : mtaskp->outEdges()) m_sb.addEdgeMC(static_cast<MTaskEdge*>(&e));
|
|
|
|
|
for (V3GraphEdge& e : mtaskp->inEdges()) m_sb.addEdgeMC(static_cast<MTaskEdge*>(&e));
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Remove the merge candidates of all edges of 'mtaskp' from the scoreboard.
|
|
|
|
|
void removeEdgeMCs(LogicMTask* mtaskp) {
|
|
|
|
|
// Note not all edges have a merge candidate:
|
|
|
|
|
// those rejected by the main contraction loop had theirs removed there.
|
|
|
|
|
for (V3GraphEdge& edge : mtaskp->outEdges()) {
|
|
|
|
|
MTaskEdge* const edgep = static_cast<MTaskEdge*>(&edge);
|
|
|
|
|
if (edgeMC(edgep)) m_sb.removeEdgeMC(edgep);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
2026-08-17 23:14:52 +02:00
|
|
|
for (V3GraphEdge& edge : mtaskp->inEdges()) {
|
|
|
|
|
MTaskEdge* const edgep = static_cast<MTaskEdge*>(&edge);
|
|
|
|
|
if (edgeMC(edgep)) m_sb.removeEdgeMC(edgep);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void makeSiblingMC(LogicMTask* ap, LogicMTask* bp) {
|
|
|
|
|
if (ap->id() < bp->id()) std::swap(ap, bp);
|
|
|
|
|
// The higher id vertex owns the association set
|
2026-08-17 23:14:52 +02:00
|
|
|
const bool first = mtaskData(ap).siblings.insert(bp).second;
|
2026-07-31 15:03:26 +01:00
|
|
|
if (first) {
|
2026-08-17 23:14:52 +02:00
|
|
|
m_sb.addSiblingMC(ap, bp);
|
2026-07-31 15:03:26 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
if (VL_UNLIKELY(m_mTaskGraph.slowAsserts())) {
|
2026-07-31 15:03:26 +01:00
|
|
|
// It's fine if we already have this SiblingMC, we may have
|
|
|
|
|
// created it earlier. Just confirm that we have associated data.
|
|
|
|
|
bool found = false;
|
|
|
|
|
for (const SiblingMC& smc : mtaskData(ap).aSiblingMCs) {
|
|
|
|
|
UASSERT_OBJ(smc.ap() == ap, ap, "Inconsistent SiblingMC");
|
|
|
|
|
if (smc.bp() == bp) found = true;
|
|
|
|
|
}
|
|
|
|
|
UASSERT_OBJ(found, ap, "Sibling not found");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <GraphWay::en N_Way, bool N_Exhaustive>
|
2026-08-17 23:14:52 +02:00
|
|
|
void addSiblingMCsFromRelatives(LogicMTask* mtaskp) {
|
2026-07-31 15:03:26 +01:00
|
|
|
constexpr GraphWay way{N_Way};
|
|
|
|
|
// Need at least 2 edges
|
|
|
|
|
auto& edges = mtaskp->edges<way>();
|
|
|
|
|
if (!edges.hasMultipleElements()) return;
|
|
|
|
|
|
|
|
|
|
std::array<LogicMTask*, PART_SIBLING_EDGE_LIMIT> neighbors;
|
|
|
|
|
|
|
|
|
|
// This is a hot method, so we want so sort as efficiently as possible. We pre-load
|
|
|
|
|
// all data (critical path cost and id) required for determining ordering into an aligned
|
|
|
|
|
// structure. There is not enough space next to these to keep a whole pointer within 16
|
|
|
|
|
// bytes, so we store an index into the neighbors buffer instead. We can then compare
|
|
|
|
|
// and swap these sorting records very efficiently. With this the standard library sorting
|
|
|
|
|
// functions are efficient enough and using more optimized methods (e.g.: sorting networks)
|
|
|
|
|
// has no measurable benefit.
|
|
|
|
|
struct alignas(16) SortingRecord final {
|
|
|
|
|
uint64_t m_cp;
|
|
|
|
|
uint32_t m_id;
|
|
|
|
|
uint8_t m_idx;
|
|
|
|
|
static_assert(PART_SIBLING_EDGE_LIMIT <= std::numeric_limits<uint8_t>::max(),
|
|
|
|
|
"m_idx must fit all indices into 'neighbors'");
|
|
|
|
|
bool operator<(const SortingRecord& that) const {
|
|
|
|
|
return m_cp < that.m_cp || (m_cp == that.m_cp && m_id < that.m_id);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
static_assert(sizeof(SortingRecord) <= 16, "How could this be padded to more than 16?");
|
|
|
|
|
|
|
|
|
|
std::array<SortingRecord, PART_SIBLING_EDGE_LIMIT> sortRecs;
|
|
|
|
|
size_t n = 0;
|
|
|
|
|
|
|
|
|
|
// Populate the buffers
|
|
|
|
|
for (V3GraphEdge& edge : mtaskp->edges<way>()) {
|
|
|
|
|
LogicMTask* const otherp = static_cast<LogicMTask*>(edge.furtherp<way>());
|
|
|
|
|
neighbors[n] = otherp;
|
|
|
|
|
sortRecs[n].m_id = otherp->id();
|
2026-08-17 23:14:52 +02:00
|
|
|
sortRecs[n].m_cp = otherp->cpInclusive<way>();
|
2026-07-31 15:03:26 +01:00
|
|
|
sortRecs[n].m_idx = n;
|
|
|
|
|
++n;
|
|
|
|
|
// Prevent nodes with huge numbers of edges from massively slowing down us down
|
|
|
|
|
if (n >= PART_SIBLING_EDGE_LIMIT) break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Don't make all possible pairs of siblings when not requested (non-exhaustive).
|
|
|
|
|
// Just make a few pairs.
|
|
|
|
|
constexpr size_t MAX_NONEXHAUSTIVE_PAIRS = 3;
|
|
|
|
|
|
|
|
|
|
if (N_Exhaustive || n <= 2 * MAX_NONEXHAUSTIVE_PAIRS) {
|
|
|
|
|
const size_t end = n & ~static_cast<size_t>(1); // Round down to even, (we want pairs)
|
|
|
|
|
std::sort(sortRecs.begin(), sortRecs.begin() + n);
|
|
|
|
|
for (size_t i = 0; i < end; i += 2) {
|
|
|
|
|
makeSiblingMC(neighbors[sortRecs[i].m_idx], neighbors[sortRecs[i + 1].m_idx]);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
constexpr size_t end = 2 * MAX_NONEXHAUSTIVE_PAIRS;
|
|
|
|
|
std::partial_sort(sortRecs.begin(), sortRecs.begin() + end, sortRecs.begin() + n);
|
|
|
|
|
for (size_t i = 0; i < end; i += 2) {
|
|
|
|
|
makeSiblingMC(neighbors[sortRecs[i].m_idx], neighbors[sortRecs[i + 1].m_idx]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
void removeSiblingMCs(LogicMTask* mtaskp) {
|
|
|
|
|
// Note: 'removeSiblingMC' unlinks the candidate from both of its MTasks' lists, so taking
|
|
|
|
|
// the front element repeatedly does terminate. It also erases the candidate from the
|
|
|
|
|
// owning (higher id) MTask's sibling set as it goes, so both the sets and the lists are
|
|
|
|
|
// left consistent, whichever side of the candidate 'mtaskp' happens to be on.
|
|
|
|
|
while (SiblingMC* const smcp = mtaskData(mtaskp).aSiblingMCs.frontp()) {
|
|
|
|
|
m_sb.removeSiblingMC(smcp);
|
|
|
|
|
}
|
|
|
|
|
while (SiblingMC* const smcp = mtaskData(mtaskp).bSiblingMCs.frontp()) {
|
|
|
|
|
m_sb.removeSiblingMC(smcp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Merge the two MTasks of 'mergeCanp'
|
|
|
|
|
void contract(MergeCandidate* mergeCanp) {
|
|
|
|
|
// The two MTasks to merge. Note the order of the two decides which of them becomes the
|
|
|
|
|
// recipient below when their costs are equal, so keep it stable.
|
|
|
|
|
LogicMTask* fromp;
|
|
|
|
|
LogicMTask* top;
|
|
|
|
|
if (const EdgeMC* const edgeMCp = mergeCanp->toEdgeMC()) {
|
|
|
|
|
fromp = edgeMCp->edgep()->fromMTaskp();
|
|
|
|
|
top = edgeMCp->edgep()->toMTaskp();
|
|
|
|
|
} else {
|
|
|
|
|
const SiblingMC* const sibMCp = mergeCanp->toSiblingMC();
|
|
|
|
|
fromp = sibMCp->bp();
|
|
|
|
|
top = sibMCp->ap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Merge the smaller mtask into the larger mtask.
|
|
|
|
|
LogicMTask* recipientp;
|
|
|
|
|
LogicMTask* donorp;
|
|
|
|
|
if (fromp->cost() > top->cost()) {
|
|
|
|
|
recipientp = fromp;
|
|
|
|
|
donorp = top;
|
|
|
|
|
} else {
|
|
|
|
|
donorp = fromp;
|
|
|
|
|
recipientp = top;
|
|
|
|
|
}
|
|
|
|
|
VL_DANGLING(fromp);
|
|
|
|
|
VL_DANGLING(top); // Use donorp and recipientp now instead
|
|
|
|
|
|
|
|
|
|
// Remove all SiblingMCs that include either MTask
|
|
|
|
|
removeSiblingMCs(recipientp);
|
|
|
|
|
removeSiblingMCs(donorp);
|
|
|
|
|
|
|
|
|
|
// Remove the EdgeMCs of both MTasks
|
|
|
|
|
removeEdgeMCs(recipientp);
|
|
|
|
|
removeEdgeMCs(donorp);
|
|
|
|
|
|
|
|
|
|
// Merge the MTasks. This redirects all edges, updates critical paths, and deletes donorp
|
|
|
|
|
m_mTaskGraph.mergeMTasks(recipientp, donorp);
|
|
|
|
|
VL_DANGLING(donorp);
|
|
|
|
|
|
|
|
|
|
// Confirm we haven't botched the CP updates.
|
|
|
|
|
m_mTaskGraph.validate();
|
|
|
|
|
|
|
|
|
|
// Add the EdgeMCs of the merged MTask
|
|
|
|
|
addEdgeMCs(recipientp);
|
|
|
|
|
|
|
|
|
|
// Finally, make new sibling pairs as needed:
|
|
|
|
|
// - prereqs and postreqs of recipientp
|
|
|
|
|
// - prereqs of recipientp's postreqs
|
|
|
|
|
// - postreqs of recipientp's prereqs
|
|
|
|
|
// Note that this depends on the updated critical paths (above).
|
|
|
|
|
addSiblingMCsFromRelatives<GraphWay::REVERSE, true>(recipientp);
|
|
|
|
|
addSiblingMCsFromRelatives<GraphWay::FORWARD, true>(recipientp);
|
|
|
|
|
unsigned edges = 0;
|
|
|
|
|
for (V3GraphEdge& edge : recipientp->outEdges()) {
|
|
|
|
|
LogicMTask* const postreqp = static_cast<LogicMTask*>(edge.top());
|
|
|
|
|
addSiblingMCsFromRelatives<GraphWay::REVERSE, false>(postreqp);
|
|
|
|
|
++edges;
|
|
|
|
|
if (edges >= PART_SIBLING_EDGE_LIMIT) break;
|
|
|
|
|
}
|
|
|
|
|
edges = 0;
|
|
|
|
|
for (V3GraphEdge& edge : recipientp->inEdges()) {
|
|
|
|
|
LogicMTask* const prereqp = static_cast<LogicMTask*>(edge.fromp());
|
|
|
|
|
addSiblingMCsFromRelatives<GraphWay::FORWARD, false>(prereqp);
|
|
|
|
|
++edges;
|
|
|
|
|
if (edges >= PART_SIBLING_EDGE_LIMIT) break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 15:03:26 +01:00
|
|
|
// CONSTRUCTORS
|
2026-08-17 23:14:52 +02:00
|
|
|
Contraction(OrderMTaskGraph& mTaskGraph, uint64_t cpLimit)
|
2026-07-31 15:03:26 +01:00
|
|
|
: m_mTaskGraph{mTaskGraph}
|
2026-08-17 23:14:52 +02:00
|
|
|
, m_scoreLimit{cpLimit} {
|
2026-07-31 15:03:26 +01:00
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Check the graph we were given is consistent.
|
|
|
|
|
m_mTaskGraph.validate();
|
2026-07-31 15:03:26 +01:00
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Figure out maximum number of MTasks
|
2026-07-31 15:03:26 +01:00
|
|
|
const uint32_t maxMTasks = []() -> uint32_t {
|
|
|
|
|
// If specified, use the given value
|
|
|
|
|
const int given = v3Global.opt.threadsMaxMTasks();
|
|
|
|
|
if (given > 0) return given;
|
|
|
|
|
// Unspecified so estimate
|
|
|
|
|
return PART_DEFAULT_MAX_MTASKS_PER_THREAD * v3Global.opt.threads();
|
|
|
|
|
}();
|
|
|
|
|
|
|
|
|
|
// Allocate and assign the auxiliary data for every LogicMTask.
|
|
|
|
|
{
|
|
|
|
|
const size_t nMTasks = m_mTaskGraph.vertices().size();
|
|
|
|
|
m_mtaskDatap.reset(new MTaskContractionData[nMTasks]);
|
|
|
|
|
size_t i = 0;
|
|
|
|
|
for (V3GraphVertex& vtx : m_mTaskGraph.vertices()) vtx.userp(&m_mtaskDatap[i++]);
|
|
|
|
|
UASSERT(i == nMTasks, "Inconsistent MTask count");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add initial candidates
|
|
|
|
|
for (V3GraphVertex& vtx : m_mTaskGraph.vertices()) {
|
2026-08-17 23:14:52 +02:00
|
|
|
for (V3GraphEdge& edge : vtx.outEdges())
|
|
|
|
|
m_sb.addEdgeMC(static_cast<MTaskEdge*>(&edge));
|
|
|
|
|
LogicMTask* const mtaskp = static_cast<LogicMTask*>(&vtx);
|
|
|
|
|
addSiblingMCsFromRelatives<GraphWay::REVERSE, true>(mtaskp);
|
|
|
|
|
addSiblingMCsFromRelatives<GraphWay::FORWARD, true>(mtaskp);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (true) {
|
2026-08-17 23:14:52 +02:00
|
|
|
// Pick the candidate yielding the lowest local critical path.
|
2026-07-31 15:03:26 +01:00
|
|
|
MergeCandidate* const mergeCanp = m_sb.best();
|
2026-08-17 23:14:52 +02:00
|
|
|
if (!mergeCanp) break; // No more candidates
|
|
|
|
|
|
|
|
|
|
// If the score has changed since it was inserted into the scoreboard, rescore it and
|
|
|
|
|
// pick again. (The real scores can differ from the one used to insert it into the
|
|
|
|
|
// scoreboard, due to merges between insertion and retrieval.)
|
|
|
|
|
const uint64_t score = mergeCanp->currentScore();
|
|
|
|
|
if (score != mergeCanp->score()) {
|
|
|
|
|
m_sb.rescore(mergeCanp);
|
2026-07-31 15:03:26 +01:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Check if the critical path limit is reached.
|
|
|
|
|
if (score > m_scoreLimit) {
|
2026-07-31 15:03:26 +01:00
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// If there are still too many MTasks, raise the limit and keep going
|
2026-07-31 15:03:26 +01:00
|
|
|
const unsigned mtaskCount = m_mTaskGraph.vertices().size();
|
|
|
|
|
if (mtaskCount > maxMTasks) {
|
|
|
|
|
m_scoreLimit = (m_scoreLimit * 120) / 100;
|
|
|
|
|
FileLine* const flp = v3Global.rootp()->fileline();
|
|
|
|
|
if (!flp->warnIsOff(V3ErrorCode::UNOPTTHREADS)) {
|
|
|
|
|
flp->v3warn(UNOPTTHREADS,
|
|
|
|
|
"Thread scheduler is unable to provide requested "
|
|
|
|
|
"parallelism; suggest asking for fewer threads.");
|
|
|
|
|
flp->modifyWarnOff(V3ErrorCode::UNOPTTHREADS, true);
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// MTasks limit and CP limit reached. Stop.
|
2026-07-31 15:03:26 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Avoid merging the entry/exit nodes. This would create serialization, by forcing the
|
|
|
|
|
// merged MTask to run before/after everything else. Empirically this helps performance
|
|
|
|
|
// in a modest way by allowing other MTasks to start earlier.
|
|
|
|
|
if (EdgeMC* const edgeMCp = mergeCanp->toEdgeMC()) {
|
|
|
|
|
MTaskEdge* const edgep = edgeMCp->edgep();
|
2026-08-17 23:14:52 +02:00
|
|
|
if (edgep->fromp() == m_mTaskGraph.entryp()
|
|
|
|
|
|| edgep->top() == m_mTaskGraph.exitp()) {
|
|
|
|
|
m_sb.removeEdgeMC(edgep);
|
2026-07-31 15:03:26 +01:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Avoid merging any edge that would create a cycle. For example suppose we begin with
|
|
|
|
|
// vertices A, B, C and edges A->B, B->C, A->C. Merging A->C would create a cycle.
|
|
|
|
|
if (mergeCanp->mergeWouldCreateCycle(m_mTaskGraph)) {
|
|
|
|
|
m_sb.removeMC(mergeCanp);
|
2026-07-31 15:03:26 +01:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Merge this candidate
|
2026-07-31 15:03:26 +01:00
|
|
|
contract(mergeCanp);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:14:52 +02:00
|
|
|
// Free all remaining merge candidates.
|
|
|
|
|
while (MergeCandidate* const mergeCanp = m_sb.best()) m_sb.removeMC(mergeCanp);
|
2026-07-31 15:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
static void apply(OrderMTaskGraph& mTaskGraph, uint64_t scoreLimit) {
|
|
|
|
|
Contraction{mTaskGraph, scoreLimit};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//######################################################################
|
|
|
|
|
// OrderMTaskGraph entry point
|
|
|
|
|
|
|
|
|
|
void OrderMTaskGraph::contract(OrderMTaskGraph& mtaskGraph, uint64_t scoreLimit) {
|
|
|
|
|
Contraction::apply(mtaskGraph, scoreLimit);
|
|
|
|
|
}
|