Files
verilator/src/V3PairingHeap.h
T
Geza Lore 7dcd4e0b65 Improve MTask coarsening in multi-threaded scheduling (#8120)
This is a large refactor of the MTask graph and coarsening algorithm, in
prep for fixing the bug described in #7913, it can also improve the
resulting multi-threaded schedule.

Two major changes:

OrderMTaskGraph now maintains the critical paths of the MTasks through
mutation. There are 2 ways to mutate the graph, which are done via
methods on the graph itself: adding an edge (used during construction,
and will be used later during fixing data hazards), or merging an MTask
into another (used during contraction). All critical path measures are
automatically updated and propagated on any mutation, so no external
algorithm needs to maintain them explicitly.

The merge candidate scoreboard used during contraction is simplified to
remove deferral of updated scores. This simplifies the code and results
in a greedily more optimal schedule. (The previous tranched rescore was
an optimization to work around the previous std::set based scoreboard,
however since the algorithm now uses an efficient PairingHeap,
verilation time is not impacted by the more accurate scoring, while
yielding better results).

Combining these two into a single patch as the code is highly
interdependent and any one change without the other would be just a
noisy transit point with unclear performance implications. Together it
should be a clear improvement.

Also added a stronger validation step run with '--debug-partition',
which checks all invariants throughout the algorithms.
2026-08-17 23:14:52 +02:00

303 lines
12 KiB
C++

// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Pairing Heap data structure
//
// 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
//
//*************************************************************************
#ifndef VERILATOR_V3PAIRINGHEAP_H_
#define VERILATOR_V3PAIRINGHEAP_H_
#include "config_build.h"
#include "verilatedos.h"
#include "V3Error.h"
//=============================================================================
// Pairing heap (max-heap) with increase key and delete.
//
// While this is written as a generic data structure, its interface and
// implementation is finely tuned for use by parallel scheduling, and is critical
// to Verilation performance, so be very careful changing anything or adding any
// new operations that would impact either memory usage, or performance of the
// existing operations. This data structure is fully deterministic, meaning
// the order in which elements with equal keys are retrieved only depends on
// the order of operations performed on the heap.
//=============================================================================
template <typename T_Key>
class PairingHeap final {
public:
struct Node;
// Just a pointer to a heap Node, but with special accessors to help keep back pointers
// consistent.
struct Link final {
Node* m_ptr = nullptr; // The managed pointer
Link() = default;
VL_UNCOPYABLE(Link);
// Make the pointer point to the target, and the target's owner pointer to this pointer
VL_ATTR_ALWINLINE void link(Node* targetp) {
m_ptr = targetp;
if (!targetp) return;
#if VL_DEBUG
UASSERT(!targetp->m_ownerpp, "Already linked");
#endif
targetp->m_ownerpp = &m_ptr;
}
// Make the pointer point to the target, and the target's owner pointer to this pointer
VL_ATTR_ALWINLINE void linkNonNull(Node* targetp) {
m_ptr = targetp;
#if VL_DEBUG
UASSERT(!targetp->m_ownerpp, "Already linked");
#endif
targetp->m_ownerpp = &m_ptr;
}
// Clear the pointer and return it's previous value
VL_ATTR_ALWINLINE Node* unlink() {
Node* const result = m_ptr;
#if VL_DEBUG
if (result) {
UASSERT(m_ptr->m_ownerpp == &m_ptr, "Bad back link");
// Not strictly necessary to clear this, but helps debugging
m_ptr->m_ownerpp = nullptr;
}
#endif
m_ptr = nullptr;
return result;
}
// Minimal convenience accessors and operators
VL_ATTR_ALWINLINE Node* ptr() const { return m_ptr; }
VL_ATTR_ALWINLINE operator bool() const { return m_ptr; }
VL_ATTR_ALWINLINE bool operator!() const { return !m_ptr; }
VL_ATTR_ALWINLINE Node* operator->() const { return m_ptr; }
VL_ATTR_ALWINLINE Node& operator*() const { return *m_ptr; }
};
// A single node in the pairing heap tree
struct Node VL_NOT_FINAL {
Link m_next; // Next in list of sibling heaps
Link m_kids; // Head of list of child heaps
Node** m_ownerpp = nullptr; // Pointer to the Link pointer pointing to this heap
T_Key m_key{}; // The key in the heap
// CONSTRUCTOR
explicit Node() = default;
VL_UNCOPYABLE(Node);
// METHODS
VL_ATTR_ALWINLINE const T_Key& key() const { return m_key; }
VL_ATTR_ALWINLINE bool operator<(const Node& that) const { return m_key < that.m_key; }
VL_ATTR_ALWINLINE bool operator>(const Node& that) const { return that.m_key < m_key; }
// Make newp take the place of this in the tree
VL_ATTR_ALWINLINE void replaceWith(Node* newp) {
*m_ownerpp = newp; // The owner pointer needs to point to the new node
if (newp) newp->m_ownerpp = m_ownerpp; // The new node needs to point to its owner
m_ownerpp = nullptr; // This node has no owner anymore
}
// Make newp take the place of this in the tree
VL_ATTR_ALWINLINE void replaceWithNonNull(Node* newp) {
*m_ownerpp = newp; // The owner pointer needs to point to the new node
newp->m_ownerpp = m_ownerpp; // The new node needs to point to its owner
m_ownerpp = nullptr; // This node has no owner anymore
}
// Yank this node out of the heap it currently is in. This node can then be safely inserted
// into another heap. Note that this leaves the heap the node is currently under in an
// inconsistent state, so you cannot access it anymore. Still this can save a remove if we
// don't care about the state of the source heap.
VL_ATTR_ALWINLINE void yank() {
m_next.link(nullptr);
m_kids.link(nullptr);
m_ownerpp = nullptr;
}
};
private:
// MEMBERS
// The root of the heap. Note: We do not reduce lists during insertion/removal etc, unless we
// absolutely have to. This means the root can become a list. This is ok, we will reduce
// lazily when requesting the minimum element.
mutable Link m_root;
// CONSTRUCTORS
VL_UNCOPYABLE(PairingHeap);
public:
explicit PairingHeap() = default;
// METHODS
bool empty() const { return !m_root; }
// Insert given node into this heap with given key.
void insert(Node* nodep, T_Key key) {
// Update key of node
nodep->m_key = key;
insert(nodep);
}
// Insert given node into this heap with key already set in the node
void insert(Node* nodep) {
#if VL_DEBUG
UASSERT(!nodep->m_ownerpp && !nodep->m_next && !nodep->m_kids, "Already linked");
#endif
// Just stick it at the front of the root list
nodep->m_next.link(m_root.unlink());
m_root.linkNonNull(nodep);
}
// Remove given node only from the heap it is contained in
void remove(Node* nodep) {
if (!nodep->m_next) {
// If the node does not have siblings, replace it with its children (might be empty).
nodep->replaceWith(nodep->m_kids.unlink());
} else if (!nodep->m_kids) {
// If it has siblings but no children, replace it with the siblings.
nodep->replaceWithNonNull(nodep->m_next.unlink());
} else {
// If it has both siblings and children, reduce the children and splice that
// reduced heap in place of this node
Node* const reducedKidsp = reduce(nodep->m_kids.unlink());
reducedKidsp->m_next.linkNonNull(nodep->m_next.unlink());
nodep->replaceWithNonNull(reducedKidsp);
}
}
// Returns the largest element in the heap
Node* max() const {
// Heap might be empty
if (!m_root) return nullptr;
// If the root have siblings reduce them
if (m_root->m_next) m_root.linkNonNull(reduce(m_root.unlink()));
// The root element is the largest
return m_root.ptr();
}
// Returns the second-largest element in the heap.
// This is only valid to call if 'max' returned a valid element.
Node* secondMax() const {
#if VL_DEBUG
UASSERT(m_root, "'max' would have returned nullptr");
UASSERT(!m_root->m_next, "'max' would have reduced");
#endif
// If there are no children, there is no second element
if (!m_root->m_kids) return nullptr;
// If there are multiple children, reduce them
if (m_root->m_kids->m_next) m_root->m_kids.linkNonNull(reduce(m_root->m_kids.unlink()));
// Return the now singular child, which is the second-largest element
return m_root->m_kids.ptr();
}
// Increase the key of the given node to the given new value
template <typename T_Update>
void increaseKey(Node* nodep, T_Update value) {
// Update the key
nodep->m_key.increase(value);
// Increasing the key of the root is easy
if (nodep == m_root.ptr()) return;
// Otherwise we do have a little work to do
if (!nodep->m_kids) {
// If the node has no children, replace it with its siblings (might be null)
nodep->replaceWith(nodep->m_next.unlink());
} else if (!nodep->m_next) {
// If the node has no siblings, replace it with its children
nodep->replaceWithNonNull(nodep->m_kids.unlink());
} else {
// The node has both children and siblings. Splice the first child in the place of the
// node, and extract the rest of the children with the node
Node* const kidsp = nodep->m_kids.unlink();
nodep->m_kids.link(kidsp->m_next.unlink());
kidsp->m_next.linkNonNull(nodep->m_next.unlink());
nodep->replaceWithNonNull(kidsp);
}
// Just stick the increased node at the front of the root list
nodep->m_next.linkNonNull(m_root.unlink());
m_root.linkNonNull(nodep);
}
private:
// Meld (merge) two heaps rooted at the given nodes, return the root of the new heap
VL_ATTR_ALWINLINE static Node* merge(Node* ap, Node* bp) {
#if VL_DEBUG
UASSERT(!ap->m_ownerpp && !ap->m_next, "Not root a");
UASSERT(!bp->m_ownerpp && !bp->m_next, "Not root b");
#endif
if (*ap > *bp) { // bp goes under ap
bp->m_next.link(ap->m_kids.unlink());
ap->m_kids.linkNonNull(bp);
return ap;
} // else ap goes under bp
ap->m_next.link(bp->m_kids.unlink());
bp->m_kids.linkNonNull(ap);
return bp;
}
// Reduces the list of nodes starting at the given node into a single node that is returned
VL_ATTR_NOINLINE static Node* reduce(Node* nodep) {
#if VL_DEBUG
UASSERT(!nodep->m_ownerpp, "Node is linked");
#endif
// If there is only one node in the list, then there is nothing to do
if (!nodep->m_next) return nodep;
// The result node
Node* resultp = nullptr;
// Pairwise merge the child nodes
while (nodep) {
// Pop off the first nodes
Node* const ap = nodep;
// If we have an odd number of nodes, prepend the unpaired one onto the result list
if (!nodep->m_next) {
ap->m_next.link(resultp);
resultp = ap;
break;
}
// Pop off the second nodes
Node* const bp = nodep->m_next.unlink();
// Keep hold of the rest of the list
nodep = bp->m_next.unlink();
// Merge the current pair
Node* const mergedp = merge(ap, bp);
// Prepend the merged pair to the result list
mergedp->m_next.link(resultp);
resultp = mergedp;
}
// Now merge-reduce the merged pairs
while (resultp->m_next) {
// Pop first two results
Node* const ap = resultp;
Node* const bp = resultp->m_next.unlink();
// Keep hold of the rest of the list
resultp = bp->m_next.unlink();
// Merge the current pair
Node* const mergedp = merge(ap, bp);
// Prepend the merged pair to the result list
mergedp->m_next.link(resultp);
resultp = mergedp;
}
// Done
return resultp;
}
};
// The PairingHeap itself should be a simple pointer and nothing more
static_assert(sizeof(PairingHeap<int>) == sizeof(PairingHeap<int>::Node*), "Should be a pointer");
#endif // Guard