Files
verilator/src/V3InlineCFuncs.cpp
T
Geza Lore 8546d5db06 Internals: Move the eval loop into the runtime library (#8225)
The loops modelling the SystemVerilog scheduling regions are no longer
generated. They now live in 'VerilatedEvalLoop' in the runtime library.
The generated model holds one as a member, passing itself to it, and
exposes each evaluation entry point to it as a pure virtual method on
VerilatedModel. The model's 'eval' and 'eval_step' remain the top level
entry points, and are backward compatible.

V3Sched no longer emits '_eval' or '_eval_settle', etc.. Instead every
evaluation entry point called from the runtime is enumerated by 'VEval',

Scheduling creates all entry points, for all scheduling regions, even if
they are empty, and the runtime eval loop calls everything
unconditionally. If regions are empty, this is simply a call to an empty
function. This will hurt performance on very small models, but should
not be noticeable on anything meaningful, so it is likely best to keep
to reduce complexity.

A scheduling entry points evaluate a single iteration and returns
whether it did any work, they are effectively the previous
`_eval_phase_*` functions.
2026-08-27 07:53:02 -04:00

498 lines
21 KiB
C++

// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Inline small CFuncs into their callers
//
// 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
//
//*************************************************************************
// V3InlineCFuncs's Transformations:
//
// Build a bipartite call graph containing function and call site vertices,
// then iterate functions leaf to root, inlining if size heuristics are met.
// Finally, remove unused functions.
//
// Two tunables control inlining:
// --inline-cfuncs <n> : Inline if size <= n
// --inline-cfuncs-product <n> : Also inline if size * call_count <= n
//
//*************************************************************************
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
#include "V3InlineCFuncs.h"
#include "V3AstUserAllocator.h"
#include "V3ExecGraph.h"
#include "V3Graph.h"
#include "V3Stats.h"
#include <vector>
VL_DEFINE_DEBUG_FUNCTIONS;
//######################################################################
// Bipartite call graph containing function and call site vertices
class InlineCFuncsFunctionVertex;
class InlineCFuncsCallSiteVertex;
class InlineCFuncsCallGraph final : public V3Graph {
public:
InlineCFuncsCallGraph()
: V3Graph{} {}
~InlineCFuncsCallGraph() override = default;
void addEdge(InlineCFuncsFunctionVertex& from, InlineCFuncsCallSiteVertex& top);
void addEdge(InlineCFuncsCallSiteVertex& from, InlineCFuncsFunctionVertex& top);
};
class EitherVertex VL_NOT_FINAL : public V3GraphVertex {
VL_RTTI_IMPL(EitherVertex, V3GraphVertex)
protected:
explicit EitherVertex(InlineCFuncsCallGraph& graph)
: V3GraphVertex{&graph} {}
};
class InlineCFuncsFunctionVertex final : public EitherVertex {
VL_RTTI_IMPL(InlineCFuncsFunctionVertex, EitherVertex)
AstCFunc* const m_cfuncp; // The function
const char* m_noInlineWyp = nullptr; // First reason the function should not be inlined
const char* m_keepWyp = nullptr; // Why the function should not be removed
size_t m_size = 0; // The size of the function
public:
InlineCFuncsFunctionVertex(InlineCFuncsCallGraph& graph, AstCFunc* cfuncp)
: EitherVertex{graph}
, m_cfuncp{cfuncp} {}
~InlineCFuncsFunctionVertex() override = default;
// ACCESSORS
AstCFunc* cfuncp() const { return m_cfuncp; }
size_t size() const { return m_size; }
void sizeInc(size_t value = 1) { m_size += value; }
bool noInline() const { return m_noInlineWyp; }
void setNoInline(const char* whyp) {
if (!m_noInlineWyp) m_noInlineWyp = whyp;
}
bool keep() const { return m_keepWyp; }
void setKeep(const char* whyp) {
if (!m_keepWyp) m_keepWyp = whyp;
}
std::string dotColor() const override {
return m_noInlineWyp ? "red" : m_keepWyp ? "orange" : "black";
}
// debug
FileLine* fileline() const override { return m_cfuncp->fileline(); }
std::string dotShape() const override { return "box"; }
std::string name() const override VL_MT_STABLE {
std::string str = cvtToHex(m_cfuncp);
str += "\n" + m_cfuncp->name();
str += "\nsize: " + std::to_string(m_size);
if (m_noInlineWyp) str += "\nNoInline: "s + m_noInlineWyp;
if (m_keepWyp) str += "\nKeep: "s + m_keepWyp;
return str;
}
};
class InlineCFuncsCallSiteVertex final : public EitherVertex {
VL_RTTI_IMPL(InlineCFuncsCallSiteVertex, EitherVertex)
AstCCall* const m_callp; // The call site
const char* m_noInlineWyp = nullptr; // First reason the function should not be inlined
public:
InlineCFuncsCallSiteVertex(InlineCFuncsCallGraph& graph, AstCCall* callp)
: EitherVertex{graph}
, m_callp{callp} {}
~InlineCFuncsCallSiteVertex() override = default;
// ACCESSORS
AstCCall* callp() const { return m_callp; }
bool noInline() const { return m_noInlineWyp; }
void setNoInline(const char* whyp) {
if (!m_noInlineWyp) m_noInlineWyp = whyp;
}
// debug
FileLine* fileline() const override { return m_callp->fileline(); }
std::string dotColor() const override { return m_noInlineWyp ? "red" : "black"; }
std::string dotShape() const override { return "ellipse"; }
std::string name() const override VL_MT_STABLE {
std::string str = cvtToHex(m_callp);
if (m_noInlineWyp) str += "\nNoInline: "s + m_noInlineWyp;
return str;
}
};
void InlineCFuncsCallGraph::addEdge(InlineCFuncsFunctionVertex& caller,
InlineCFuncsCallSiteVertex& callsite) {
UASSERT_OBJ(callsite.inEmpty(), &callsite, "Call site should have at most one incoming edge");
new V3GraphEdge{this, &caller, &callsite, 1, true}; // Can cut caller -> callsite
}
void InlineCFuncsCallGraph::addEdge(InlineCFuncsCallSiteVertex& callsite,
InlineCFuncsFunctionVertex& callee) {
UASSERT_OBJ(callsite.outEmpty(), &callsite, "Call site should have at most one outgoing edge");
new V3GraphEdge{this, &callsite, &callee, 1, false};
}
//######################################################################
class InlineCFuncsVisitor final : public VNVisitor {
// NODE STATE
// AstCFunc::user1p() -> InlineCFuncsFunctionVertex*, the function vertex
// AstCCall::user1p() -> InlineCFuncsCallSiteVertex*, the call site vertex
// AstVar::user2p() -> AstVar*, the cloned inlined local variable
const VNUser1InUse m_user1InUse;
// STATE
InlineCFuncsCallGraph m_graph; // The call graph
VDouble0 m_statCallsInlined; // Number of calls inlined
VDouble0 m_statFuncsInlined; // Number of functions inlined at least once
VDouble0 m_statFuncsRemoved; // Number of fully-inlined functions removed
// Size threshold: always inline if size <= this
const size_t m_sizeThreshold = v3Global.opt.inlineCFuncs();
// Product threshold: inline if size * calls <= this
const size_t m_prodThreshold = v3Global.opt.inlineCFuncsProduct();
// Maximum size of caller to consider inlining into
const size_t m_maxSizeCFunc = []() -> size_t {
int maxCFunc = v3Global.opt.outputSplitCFuncs();
int maxFile = v3Global.opt.outputSplit();
if (maxCFunc <= 0) maxCFunc = std::numeric_limits<int>::max();
if (maxFile <= 0) maxFile = std::numeric_limits<int>::max();
return std::min(maxCFunc, maxFile);
}();
const size_t m_maxSizeTrace = []() -> size_t {
int maxTrace = v3Global.opt.outputSplitCTrace();
int maxFile = v3Global.opt.outputSplit();
if (maxTrace <= 0) maxTrace = std::numeric_limits<int>::max();
if (maxFile <= 0) maxFile = std::numeric_limits<int>::max();
return std::min(maxTrace, maxFile);
}();
InlineCFuncsFunctionVertex* m_cfuncVtxp = nullptr; // Vertex of currently iterated function
bool m_inExecGraph = false; // True while inside an AstExecGraph subtree
// METHODS
InlineCFuncsFunctionVertex* getInlineCFuncsFunctionVertexp(AstCFunc* cfuncp) {
if (!cfuncp->user1p()) cfuncp->user1p(new InlineCFuncsFunctionVertex{m_graph, cfuncp});
return cfuncp->user1u().to<InlineCFuncsFunctionVertex*>();
}
InlineCFuncsCallSiteVertex* getInlineCFuncsCallSiteVertexp(AstCCall* callp) {
if (!callp->user1p()) callp->user1p(new InlineCFuncsCallSiteVertex{m_graph, callp});
return callp->user1u().to<InlineCFuncsCallSiteVertex*>();
}
AstCLocalScope* inlineCall(AstCFunc* const callerp, //
AstCCall* const callp, //
AstCFunc* const calleep, //
const size_t seqNum) {
UINFO(6, "Inlining CFunc " << calleep->name() << " into " << callerp->name()
<< " at call site " << callp);
AstNodeStmt* const callSitep = VN_AS(callp->backp(), StmtExpr);
++m_statCallsInlined;
// Callee might be empty, just delete the call
if (!calleep->stmtsp()) {
VL_DO_DANGLING(pushDeletep(callSitep->unlinkFrBack()), callSitep);
return nullptr;
}
// Replace call site with a local scope
FileLine* const flp = callSitep->fileline();
AstCLocalScope* const lscopep = new AstCLocalScope{flp, nullptr};
callSitep->replaceWith(lscopep);
VL_DO_DANGLING(pushDeletep(callSitep), callSitep);
lscopep->addStmtsp(new AstComment{flp, "Inlined CFunc: " + calleep->name()});
// Although it's in a local scope, we still make names of cloned locals unique
const std::string varPrefix
= "__Vinline_" + std::to_string(seqNum) + "_" + calleep->name() + "_";
// AstVar::user2p() -> AstVar*, the cloned inlined local variable
const VNUser2InUse user2InUse;
// Clone local variables, add them to the local scope
for (AstVar* varp = calleep->varsp(); varp; varp = VN_AS(varp->nextp(), Var)) {
AstVar* const newVarp = varp->cloneTree(false);
newVarp->name(varPrefix + varp->name());
lscopep->addStmtsp(newVarp);
varp->user2p(newVarp);
}
// Clone the function body
AstNode* const bodyp = calleep->stmtsp()->cloneTree(true);
lscopep->addStmtsp(bodyp);
// Retarget local variable references to the cloned locals
// Rename locals defined in the body, TODO: there should be none after #6280
// Reset vertex pointers on calls
bodyp->foreachAndNext([&](AstNode* nodep) {
if (AstVarRef* const refp = VN_CAST(nodep, VarRef)) {
if (AstVar* const varp = VN_AS(refp->varp()->user2p(), Var)) refp->varp(varp);
} else if (AstVar* const varp = VN_CAST(nodep, Var)) {
varp->name(varPrefix + varp->name());
} else if (AstCCall* const callp = VN_CAST(nodep, CCall)) {
callp->user1p(nullptr);
}
});
// Return the local scope
return lscopep;
}
void doInlining() {
// Need to gather vertices as we are changing the graph
std::vector<InlineCFuncsFunctionVertex*> m_fVtxps;
for (V3GraphVertex& vtx : m_graph.vertices()) {
if (InlineCFuncsFunctionVertex* const fVtxp = vtx.cast<InlineCFuncsFunctionVertex>()) {
m_fVtxps.emplace_back(fVtxp);
}
}
// Iterate functions leaf to root
for (InlineCFuncsFunctionVertex* const calleeVtxp : vlstd::reverse_view(m_fVtxps)) {
// Should we inline this function?
if (calleeVtxp->noInline()) continue; // Told not to
// Check size heuristics
const bool doIt = [&]() {
// Inline if small enough
if (calleeVtxp->size() <= m_sizeThreshold) return true;
// Inline if not too much bloat
const size_t nCalls = calleeVtxp->inEdges().size();
if (nCalls * calleeVtxp->size() <= m_prodThreshold) return true;
// Otherwise don't inline
return false;
}();
if (!doIt) continue;
// Ok, attempt to inline call sites
size_t nInlined = 0;
for (const V3GraphEdge* const edgep : calleeVtxp->inEdges().unlinkable()) {
InlineCFuncsCallSiteVertex* const callVtxp
= edgep->fromp()->as<InlineCFuncsCallSiteVertex>();
AstCFunc* const calleep = calleeVtxp->cfuncp();
AstCCall* const callp = callVtxp->callp();
UINFO(6, "Consider inlining " << calleep->name() << " at call site " << callp);
// Should we inline this call site?
if (callVtxp->noInline()) continue; // Told not to
if (callVtxp->inEmpty()) continue; // Don't know where it's called from
// Pick up the caller
UASSERT_OBJ(callVtxp->inSize1(), callVtxp->callp(),
"Expected exactly one input edge for call site");
InlineCFuncsFunctionVertex* const callerVtxp
= callVtxp->inEdges().frontp()->fromp()->as<InlineCFuncsFunctionVertex>();
AstCFunc* const callerp = callerVtxp->cfuncp();
// Don't make a function bigger than the limit
const size_t limit = callerp->isTrace() ? m_maxSizeTrace : m_maxSizeCFunc;
if (callerVtxp->size() + calleeVtxp->size() > limit) continue;
// Can't do it if it's in a different scope, self pointers differ
if (callerp->scopep() != calleep->scopep()) continue;
// Inline it
if (!nInlined) ++m_statFuncsInlined;
AstNode* const inlinedp = inlineCall(callerp, callp, calleep, nInlined++);
// Need to adjust the graph:
// 1. Delete inlined call site
VL_DO_DANGLING(callVtxp->unlinkDelete(&m_graph), callVtxp);
// 2. Add new inlined call sites - also increments size of caller
VL_RESTORER(m_cfuncVtxp);
m_cfuncVtxp = callerVtxp;
if (inlinedp) iterateChildrenConst(inlinedp);
}
}
}
void removeUnusedFuncs() {
// Iterate root to leaves
for (V3GraphVertex* const vtxp : m_graph.vertices().unlinkable()) {
InlineCFuncsFunctionVertex* const fVtxp = vtxp->cast<InlineCFuncsFunctionVertex>();
if (!fVtxp) continue;
// Keep if still called
if (!fVtxp->inEmpty()) continue;
// Keep for other reasons
if (fVtxp->keep()) continue;
AstCFunc* const funcp = fVtxp->cfuncp();
UINFO(6, "Removing unused CFunc " << funcp);
++m_statFuncsRemoved;
// Unlink all call sites
for (const V3GraphEdge* const edgep : vtxp->outEdges().unlinkable()) {
edgep->top()->unlinkEdges(&m_graph);
}
// Delete function vertex
vtxp->unlinkDelete(&m_graph);
// Delete the function
VL_DO_DANGLING(pushDeletep(funcp->unlinkFrBack()), funcp);
}
// Delete inlined/deleted call site vertices (for debugging only)
for (V3GraphVertex* const vtxp : m_graph.vertices().unlinkable()) {
InlineCFuncsCallSiteVertex* const cVtxp = vtxp->cast<InlineCFuncsCallSiteVertex>();
if (!cVtxp) continue;
if (!cVtxp->inEmpty()) continue;
if (!cVtxp->outEmpty()) continue;
vtxp->unlinkDelete(&m_graph);
}
}
// VISITORS
void visit(AstCFunc* nodep) override {
// Create the function vertex
InlineCFuncsFunctionVertex* const vtxp = getInlineCFuncsFunctionVertexp(nodep);
// Check if the function itself is not inlineable
if (nodep->rtnTypeVoid() != "void") vtxp->setNoInline("Not void");
if (nodep->dpiImportPrototype()) vtxp->setNoInline("DPI import prototype");
if (nodep->recursive()) vtxp->setNoInline("Recursive");
if (nodep->argsp()) vtxp->setNoInline("Has arguments");
if (nodep->isVirtual()) vtxp->setNoInline("Virtual method");
// Check if the function should not be removed
if (nodep->entryPoint()) vtxp->setKeep("Entry point");
if (nodep->dpiImportPrototype()) vtxp->setKeep("DPI import prototype");
if (nodep->dpiExportDispatcher()) vtxp->setKeep("DPI export implementation");
if (nodep->isVirtual()) vtxp->setKeep("Virtual method");
// Iterate children
VL_RESTORER(m_cfuncVtxp);
m_cfuncVtxp = vtxp;
iterateChildrenConst(nodep);
}
// Inlineable calls
void visit(AstCCall* nodep) override {
if (m_cfuncVtxp) m_cfuncVtxp->sizeInc();
AstCFunc* const calleep = nodep->funcp();
// Create the call site vertex
InlineCFuncsCallSiteVertex* const vtxp = getInlineCFuncsCallSiteVertexp(nodep);
// Check if the call site is not inlineable
if (!VN_IS(nodep->backp(), StmtExpr)) vtxp->setNoInline("Not in statement position");
if (m_inExecGraph) vtxp->setNoInline("In ExecGraph");
if (calleep->isVirtual()) vtxp->setNoInline("Virtual method");
// Add caller/callee edges
if (m_cfuncVtxp) m_graph.addEdge(*m_cfuncVtxp, *vtxp);
m_graph.addEdge(*vtxp, *getInlineCFuncsFunctionVertexp(calleep));
// Iterate children
iterateChildrenConst(nodep);
}
void visit(AstNodeCCall* nodep) override {
if (m_cfuncVtxp) m_cfuncVtxp->sizeInc();
getInlineCFuncsFunctionVertexp(nodep->funcp())->setKeep("Called elsewhere");
iterateChildrenConst(nodep);
}
void visit(AstAddrOfCFunc* nodep) override {
if (m_cfuncVtxp) m_cfuncVtxp->sizeInc();
getInlineCFuncsFunctionVertexp(nodep->funcp())->setKeep("Referenced by AddressOfCFunc");
iterateChildrenConst(nodep);
}
// Nodes preventing inlining
void visit(AstTraceDecl* nodep) override {
// Referenced by TraceInc
if (m_cfuncVtxp) m_cfuncVtxp->setNoInline("Contains TraceDecl");
if (AstCCall* const callp = nodep->dtypeCallp()) {
getInlineCFuncsCallSiteVertexp(callp)->setNoInline("Referenced by TraceDecl");
}
iterateChildrenConst(nodep);
}
void visit(AstExecGraph* nodep) override {
// AstExecGraph is not cloneable, so can't inline the containing function
if (m_cfuncVtxp) m_cfuncVtxp->setNoInline("Contains ExecGraph");
// Also mark functions referenced in the dependency graph
for (const V3GraphVertex& vtx : nodep->depGraphp()->vertices()) {
getInlineCFuncsFunctionVertexp(vtx.as<ExecMTask>()->funcp())
->setKeep("MTask function");
}
VL_RESTORER(m_inExecGraph);
m_inExecGraph = true;
iterateChildrenConst(nodep);
}
void visit(AstCStmt* nodep) override {
// Can reference anything in text
if (m_cfuncVtxp) m_cfuncVtxp->setNoInline("Contains CStmt");
iterateChildrenConst(nodep);
}
void visit(AstCExpr* nodep) override {
// Can reference anything in text
if (m_cfuncVtxp) m_cfuncVtxp->setNoInline("Contains CExpr");
iterateChildrenConst(nodep);
}
void visit(AstCStmtUser* nodep) override {
// Can reference anything in text
if (m_cfuncVtxp) m_cfuncVtxp->setNoInline("Contains CStmtUser");
iterateChildrenConst(nodep);
}
void visit(AstCExprUser* nodep) override {
// Can reference anything in text
if (m_cfuncVtxp) m_cfuncVtxp->setNoInline("Contains CExprUser");
iterateChildrenConst(nodep);
}
void visit(AstCReturn* nodep) override {
if (m_cfuncVtxp) m_cfuncVtxp->setNoInline("Contains CReturn");
iterateChildrenConst(nodep);
}
// Base node
void visit(AstNode* nodep) override {
if (m_cfuncVtxp) m_cfuncVtxp->sizeInc();
iterateChildrenConst(nodep);
}
public:
// CONSTRUCTORS
explicit InlineCFuncsVisitor(AstNetlist* nodep) {
// Phase 1: Build call graph
iterateConst(nodep);
// Make acyclic in case there is recursion
m_graph.acyclic(V3GraphEdge::followAlwaysTrue);
// Order vertices (any topological order is fine)
m_graph.order();
if (dumpGraphLevel() >= 6) m_graph.dumpDotFilePrefixed("inlinecfuncs-graph");
// Phase 2: Inline calls
doInlining();
if (dumpGraphLevel() >= 6) m_graph.dumpDotFilePrefixed("inlinecfuncs-inlined");
// Phase 3: Remove unused functions
removeUnusedFuncs();
if (dumpGraphLevel() >= 6) m_graph.dumpDotFilePrefixed("inlinecfuncs-kept");
}
~InlineCFuncsVisitor() override {
V3Stats::addStat("Optimizations, Inline CFuncs, calls inlined", m_statCallsInlined);
V3Stats::addStat("Optimizations, Inline CFuncs, functions inlined", m_statFuncsInlined);
V3Stats::addStat("Optimizations, Inline CFuncs, functions removed", m_statFuncsRemoved);
}
};
//######################################################################
// InlineCFuncs class functions
void V3InlineCFuncs::inlineAll(AstNetlist* nodep) {
UINFO(2, __FUNCTION__ << ":");
// Don't inline when profiling per-function (it would lose granularity)
if (v3Global.opt.profCFuncs()) return;
{ InlineCFuncsVisitor{nodep}; } // Destruct before checking
V3Global::dumpCheckGlobalTree("inlinecfuncs", 0, dumpTreeEitherLevel() >= 6);
}