From e63ed0a93173176d539a758eb86be57ffddc02f4 Mon Sep 17 00:00:00 2001
From: Geza Lore <gezalore@gmail.com>
Date: Tue, 2 Sep 2025 22:21:24 +0100
Subject: [PATCH] Internals: Manage DfgVertex user data via a typed map (#6356)

Add DfgUserMap as a handle around the one pointer worth of algorithm
specific 'user' storage in each DfgVertex. This reduces verbosity,
improves type safety and correctness. Also enables us to remove one
pointer from DfgVertex to reduce memory use. No functional change.
---
 src/V3Dfg.cpp              |  24 +--
 src/V3Dfg.h                | 294 +++++++++++++++++++++++--------------
 src/V3DfgBreakCycles.cpp   | 286 +++++++++++++++++++-----------------
 src/V3DfgColorSCCs.cpp     | 129 ++++++++--------
 src/V3DfgDecomposition.cpp |  70 ++++-----
 src/V3DfgPasses.cpp        |  88 +++++------
 src/V3DfgPasses.h          |   4 +-
 src/V3DfgPeephole.cpp      |  30 ++--
 src/V3DfgSynthesize.cpp    |   6 +-
 9 files changed, 503 insertions(+), 428 deletions(-)

diff --git a/src/V3Dfg.cpp b/src/V3Dfg.cpp
index 4d2a8e3429d..af50685a2b1 100644
--- a/src/V3Dfg.cpp
+++ b/src/V3Dfg.cpp
@@ -223,20 +223,26 @@ void DfgGraph::mergeGraphs(std::vector<std::unique_ptr<DfgGraph>>&& otherps) {
             }
             // Otherwise they will be moved
             vtxp->nodep()->user2p(vtxp);
-            vtxp->m_userCnt = 0;
-            vtxp->m_graphp = this;
+            vtxp->m_userGeneration = 0;
+#ifdef VL_DEBUG
+            vtxp->m_dfgp = this;
+#endif
         }
         m_varVertices.splice(m_varVertices.end(), otherp->m_varVertices);
         // Process constants
         for (DfgConst& vtx : otherp->m_constVertices) {
-            vtx.m_userCnt = 0;
-            vtx.m_graphp = this;
+            vtx.m_userGeneration = 0;
+#ifdef VL_DEBUG
+            vtx.m_dfgp = this;
+#endif
         }
         m_constVertices.splice(m_constVertices.end(), otherp->m_constVertices);
         // Process operations
         for (DfgVertex& vtx : otherp->m_opVertices) {
-            vtx.m_userCnt = 0;
-            vtx.m_graphp = this;
+            vtx.m_userGeneration = 0;
+#ifdef VL_DEBUG
+            vtx.m_dfgp = this;
+#endif
         }
         m_opVertices.splice(m_opVertices.end(), otherp->m_opVertices);
         // Update graph sizes
@@ -586,8 +592,8 @@ bool DfgVertex::equals(const DfgVertex& that, EqualsCache& cache) const {
     return result >> 1;
 }
 
-V3Hash DfgVertex::hash() {
-    V3Hash& result = user<V3Hash>();
+V3Hash DfgVertex::hash(DfgUserMap<V3Hash>& cache) {
+    V3Hash& result = cache[this];
     if (!result.value()) {
         V3Hash hash{selfHash()};
         // Variables are defined by themselves, so there is no need to hash them further
@@ -597,7 +603,7 @@ V3Hash DfgVertex::hash() {
             hash += m_type;
             hash += size();
             foreachSource([&](DfgVertex& vtx) {
-                hash += vtx.hash();
+                hash += vtx.hash(cache);
                 return false;
             });
         }
diff --git a/src/V3Dfg.h b/src/V3Dfg.h
index bab4a0498f9..62fc4749418 100644
--- a/src/V3Dfg.h
+++ b/src/V3Dfg.h
@@ -54,10 +54,18 @@
 #define VL_NOT_FINAL  // This #define fixes broken code folding in the CLion IDE
 #endif
 
+// Can T be stored in the memory allocted for U?
+template <typename T, typename U>
+inline constexpr bool fitsSpaceAllocatedFor() {
+    return sizeof(T) <= sizeof(U) && alignof(T) <= alignof(U);
+}
+
 class DfgEdge;
 class DfgVertex;
 class DfgGraph;
 class DfgVisitor;
+template <typename T_User, bool = fitsSpaceAllocatedFor<T_User, void*>()>
+class DfgUserMap;
 
 // Specialization of std::hash for a std::pair<const DfgVertex*, const DfgVertex*> for use below
 template <>
@@ -191,8 +199,8 @@ class DfgVertex VL_NOT_FINAL {
     friend class DfgGraph;
     friend class DfgEdge;
     friend class DfgVisitor;
-
-    using UserDataStorage = void*;  // Storage allocated for user data
+    template <typename, bool>
+    friend class DfgUserMap;
 
     // STATE
     V3ListLinks<DfgVertex> m_links;  // V3List links in the DfgGraph
@@ -201,10 +209,16 @@ class DfgVertex VL_NOT_FINAL {
 
     FileLine* const m_filelinep;  // Source location
     AstNodeDType* m_dtypep;  // Data type of the result of this vertex - mutable for efficiency
-    DfgGraph* m_graphp;  // The containing DfgGraph
     const VDfgType m_type;  // Vertex type tag
-    uint32_t m_userCnt = 0;  // User data generation number
-    UserDataStorage m_userDataStorage = nullptr;  // User data storage
+
+    // The only way to access thes is via DfgUserMap, so mutable is appropriate,
+    // the map can change while the keys (DfgVertex) are const.
+    mutable uint32_t m_userGeneration = 0;  // User data generation number
+    mutable void* m_userStorage = nullptr;  // User data storage - one pointer worth
+
+#ifdef VL_DEBUG
+    DfgGraph* m_dfgp = nullptr;  // Graph this vertex belongs to
+#endif
 
     // METHODS
     // Visitor accept method
@@ -252,8 +266,10 @@ class DfgVertex VL_NOT_FINAL {
     FileLine* fileline() const { return m_filelinep; }
     // The data type of the result of the vertex
     AstNodeDType* dtypep() const { return m_dtypep; }
-    // Is it a packed type (instead of an array)
+    // Is it a packed type
     bool isPacked() const { return VN_IS(dtypep(), BasicDType); }
+    // Is it an array type
+    bool isArray() const { return !isPacked(); }
     // Width of result
     uint32_t width() const {
         UASSERT_OBJ(isPacked(), this, "non-packed has no 'width()'");
@@ -265,19 +281,6 @@ class DfgVertex VL_NOT_FINAL {
         return VN_AS(dtypep(), UnpackArrayDType)->elementsConst();
     }
 
-    // Retrieve user data, constructing it fresh on first try.
-    template <typename T_User>
-    T_User& user();
-    // Retrieve user data, must be current.
-    template <typename T_User>
-    const T_User& getUser() const;
-    // Retrieve user data, must be current.
-    template <typename T_User>
-    T_User& getUser();
-    // Set user data, becomes current.
-    template <typename T_User>
-    void setUser(T_User value);
-
     // Cache type for 'equals' below
     using EqualsCache = std::unordered_map<std::pair<const DfgVertex*, const DfgVertex*>, uint8_t>;
 
@@ -295,8 +298,8 @@ class DfgVertex VL_NOT_FINAL {
     }
 
     // Hash of vertex (depends on this vertex and all upstream vertices feeding into this vertex).
-    // Uses user data for caching hashes
-    V3Hash hash() VL_MT_DISABLED;
+    // Uses the given DfgUserMap for caching hashes
+    V3Hash hash(DfgUserMap<V3Hash>& cache) VL_MT_DISABLED;
 
     // Predicate: has 1 or more sinks
     bool hasSinks() const { return !m_sinks.empty(); }
@@ -465,34 +468,7 @@ class DfgVisitor VL_NOT_FINAL {
 //------------------------------------------------------------------------------
 // Dataflow graph
 class DfgGraph final {
-    friend class DfgVertex;
-
-    // TYPES
-
-    // RAII handle for DfgVertex user data
-    class UserDataInUse final {
-        DfgGraph* m_graphp;  // The referenced graph
-
-    public:
-        // cppcheck-suppress noExplicitConstructor
-        UserDataInUse(DfgGraph* graphp)
-            : m_graphp{graphp} {}
-        // cppcheck-suppress noExplicitConstructor
-        UserDataInUse(UserDataInUse&& that) {
-            UASSERT(that.m_graphp, "Moving from empty");
-            m_graphp = std::exchange(that.m_graphp, nullptr);
-        }
-        VL_UNCOPYABLE(UserDataInUse);
-        UserDataInUse& operator=(UserDataInUse&& that) {
-            UASSERT(that.m_graphp, "Moving from empty");
-            m_graphp = std::exchange(that.m_graphp, nullptr);
-            return *this;
-        }
-
-        ~UserDataInUse() {
-            if (m_graphp) m_graphp->m_userCurrent = 0;
-        }
-    };
+    friend class DfgUserMapBase;
 
     // MEMBERS
 
@@ -503,16 +479,18 @@ class DfgGraph final {
     DfgVertex::List<DfgVertexVar> m_varVertices;  // The variable vertices in the graph
     DfgVertex::List<DfgConst> m_constVertices;  // The constant vertices in the graph
     DfgVertex::List<DfgVertex> m_opVertices;  // The operation vertices in the graph
-
     size_t m_size = 0;  // Number of vertices in the graph
-    uint32_t m_userCurrent = 0;  // Vertex user data generation number currently in use
-    uint32_t m_userCnt = 0;  // Vertex user data generation counter
     // Parent of the graph (i.e.: the module containing the logic represented by this graph),
     // or nullptr when run after V3Scope
     AstModule* const m_modulep;
     const std::string m_name;  // Name of graph - need not be unique
     std::string m_tmpNameStub{""};  // Name stub for temporary variables - computed lazy
 
+    // The only way to access thes is via DfgUserMap, so mutable is appropriate,
+    // the map can change while the graph is const.
+    mutable bool m_vertexUserInUse = false;  // Vertex user data currently in use
+    mutable uint32_t m_vertexUserGeneration = 0;  // Vertex user data generation counter
+
 public:
     // CONSTRUCTOR
     explicit DfgGraph(AstModule* modulep, const string& name = "") VL_MT_DISABLED;
@@ -528,14 +506,9 @@ class DfgGraph final {
     // Name of this graph
     const string& name() const { return m_name; }
 
-    // Reset Vertex user data
-    UserDataInUse userDataInUse() {
-        UASSERT(!m_userCurrent, "Conflicting use of DfgVertex user data");
-        ++m_userCnt;
-        UASSERT(m_userCnt, "'m_userCnt' overflow");
-        m_userCurrent = m_userCnt;
-        return UserDataInUse{this};
-    }
+    // Create a new DfgUserMap
+    template <typename T_User>
+    inline DfgUserMap<T_User> makeUserMap() const;
 
     // Access to vertex lists
     DfgVertex::List<DfgVertexVar>& varVertices() { return m_varVertices; }
@@ -547,6 +520,9 @@ class DfgGraph final {
 
     // Add DfgVertex to this graph (assumes not yet contained).
     void addVertex(DfgVertex& vtx) {
+#ifdef VL_DEBUG
+        UASSERT_OBJ(!vtx.m_dfgp, &vtx, "Vertex already in a graph");
+#endif
         // Note: changes here need to be replicated in DfgGraph::mergeGraphs
         ++m_size;
         if (DfgConst* const cVtxp = vtx.cast<DfgConst>()) {
@@ -556,12 +532,17 @@ class DfgGraph final {
         } else {
             m_opVertices.linkBack(&vtx);
         }
-        vtx.m_userCnt = 0;
-        vtx.m_graphp = this;
+        vtx.m_userGeneration = 0;
+#ifdef VL_DEBUG
+        vtx.m_dfgp = this;
+#endif
     }
 
     // Remove DfgVertex form this graph (assumes it is contained).
     void removeVertex(DfgVertex& vtx) {
+#ifdef VL_DEBUG
+        UASSERT_OBJ(vtx.m_dfgp == this, &vtx, "Vertex not in this graph");
+#endif
         // Note: changes here need to be replicated in DfgGraph::mergeGraphs
         --m_size;
         if (DfgConst* const cVtxp = vtx.cast<DfgConst>()) {
@@ -571,8 +552,10 @@ class DfgGraph final {
         } else {
             m_opVertices.unlink(&vtx);
         }
-        vtx.m_userCnt = 0;
-        vtx.m_graphp = nullptr;
+        vtx.m_userGeneration = 0;
+#ifdef VL_DEBUG
+        vtx.m_dfgp = nullptr;
+#endif
     }
 
     // Calls given function 'f' for each vertex in the graph. It is safe to manipulate any vertices
@@ -655,6 +638,137 @@ class DfgGraph final {
     sinkCone(const std::vector<const DfgVertex*>&) const VL_MT_DISABLED;
 };
 
+//------------------------------------------------------------------------------
+// Map from DfgVertices to T_Value implemeneted via DfgVertex::m_userStorage
+
+// Base class with common behavour
+class DfgUserMapBase VL_NOT_FINAL {
+    template <typename, bool>
+    friend class DfgUserMap;
+
+protected:
+    // STATE
+    const DfgGraph* m_dfgp;  // The graph this map is for
+    // The current generation number
+    const uint32_t m_currentGeneration;
+
+    // CONSTRUCTOR
+    explicit DfgUserMapBase(const DfgGraph* dfgp)
+        : m_dfgp{dfgp}
+        , m_currentGeneration{++m_dfgp->m_vertexUserGeneration} {
+        UASSERT(m_currentGeneration, "DfgGraph user data genartion number overflow");
+        UASSERT(!m_dfgp->m_vertexUserInUse, "DfgUserMap already in use for this DfgGraph");
+        m_dfgp->m_vertexUserInUse = true;
+    }
+    VL_UNCOPYABLE(DfgUserMapBase);
+    DfgUserMapBase(DfgUserMapBase&& that)
+        : m_dfgp{that.m_dfgp}
+        , m_currentGeneration{that.m_currentGeneration} {
+        that.m_dfgp = nullptr;
+    }
+    DfgUserMapBase& operator=(DfgUserMapBase&&) = delete;
+
+public:
+    ~DfgUserMapBase() {
+        if (m_dfgp) m_dfgp->m_vertexUserInUse = false;
+    }
+};
+
+// Specialization where T_Value fits in DfgVertex::m_userStorage directly
+template <typename T_Value>
+class DfgUserMap<T_Value, true> final : public DfgUserMapBase {
+    static_assert(fitsSpaceAllocatedFor<T_Value, decltype(DfgVertex::m_userStorage)>(),
+                  "'T_Value' does not fit 'DfgVertex::m_userStorage'");
+    friend class DfgGraph;
+
+    // CONSTRUCTOR
+    explicit DfgUserMap(const DfgGraph* dfgp)
+        : DfgUserMapBase{dfgp} {}
+    VL_UNCOPYABLE(DfgUserMap);
+    DfgUserMap& operator=(DfgUserMap&&) = delete;
+
+public:
+    DfgUserMap(DfgUserMap&&) = default;
+    ~DfgUserMap() = default;
+
+    // METHODS
+    // Retrieve mapped value for 'vtx', value initializing it on first access
+    T_Value& operator[](const DfgVertex& vtx) {
+#ifdef VL_DEBUG
+        UASSERT_OBJ(vtx.m_dfgp == m_dfgp, &vtx, "Vertex not in this graph");
+#endif
+        T_Value* const storagep = reinterpret_cast<T_Value*>(&vtx.m_userStorage);
+        if (vtx.m_userGeneration != m_currentGeneration) {
+            new (storagep) T_Value{};
+            vtx.m_userGeneration = m_currentGeneration;
+        }
+        return *storagep;
+    }
+    // Same as above with pointer as key
+    T_Value& operator[](const DfgVertex* vtxp) { return (*this)[*vtxp]; }
+
+    // Retrieve mapped value of 'vtx', must be alerady present
+    T_Value& at(const DfgVertex& vtx) const {
+#ifdef VL_DEBUG
+        UASSERT_OBJ(vtx.m_dfgp == m_dfgp, &vtx, "Vertex not in this graph");
+#endif
+        UASSERT_OBJ(vtx.m_userGeneration == m_currentGeneration, &vtx, "Vertex not in map");
+        T_Value* const storagep = reinterpret_cast<T_Value*>(&vtx.m_userStorage);
+        return *storagep;
+    }
+    // Same as above with pointer as key
+    T_Value& at(const DfgVertex* vtxp) const { return (*this).at(*vtxp); }
+};
+
+// Specialization where T_Value does not fit in DfgVertex::m_userStorage directly
+template <typename T_Value>
+class DfgUserMap<T_Value, false> final : public DfgUserMapBase {
+    static_assert(fitsSpaceAllocatedFor<T_Value*, decltype(DfgVertex::m_userStorage)>(),
+                  "'T_Value*' does not fit 'DfgVertex::m_userStorage'");
+    friend class DfgGraph;
+
+    // STATE
+    std::deque<T_Value> m_storage;  // Storage for T_Value instances
+
+    // CONSTRUCTOR
+    explicit DfgUserMap(const DfgGraph* dfgp)
+        : DfgUserMapBase{dfgp} {}
+    VL_UNCOPYABLE(DfgUserMap);
+    DfgUserMap& operator=(DfgUserMap&&) = delete;
+
+public:
+    DfgUserMap(DfgUserMap&&) = default;
+    ~DfgUserMap() = default;
+
+    // METHODS
+    // Retrieve mapped value for 'vtx', value initializing it on first access
+    T_Value& operator[](const DfgVertex& vtx) {
+#ifdef VL_DEBUG
+        UASSERT_OBJ(vtx.m_dfgp == m_dfgp, &vtx, "Vertex not in this graph");
+#endif
+        T_Value*& storagepr = reinterpret_cast<T_Value*&>(vtx.m_userStorage);
+        if (vtx.m_userGeneration != m_currentGeneration) {
+            m_storage.emplace_back();
+            storagepr = &m_storage.back();
+            vtx.m_userGeneration = m_currentGeneration;
+        }
+        return *storagepr;
+    }
+    // Same as above with pointer as key
+    T_Value& operator[](const DfgVertex* vtxp) { return (*this)[*vtxp]; }
+
+    // Retrieve mapped value of 'vtx', must be alerady present
+    T_Value& at(const DfgVertex& vtx) const {
+#ifdef VL_DEBUG
+        UASSERT_OBJ(vtx.m_dfgp == m_dfgp, &vtx, "Vertex not in this graph");
+#endif
+        UASSERT_OBJ(vtx.m_userGeneration == m_currentGeneration, &vtx, "Vertex not in map");
+        return *reinterpret_cast<T_Value*&>(vtx.m_userStorage);
+    }
+    // Same as above with pointer as key
+    T_Value& at(const DfgVertex* vtxp) const { return (*this).at(*vtxp); }
+};
+
 //------------------------------------------------------------------------------
 // Inline method definitions
 
@@ -684,57 +798,11 @@ void DfgEdge::relinkSrcp(DfgVertex* srcp) {
 
 // }}}
 
-// DfgVertex {{{
+// DfgGraph {{{
 
 template <typename T_User>
-T_User& DfgVertex::user() {
-    static_assert(sizeof(T_User) <= sizeof(UserDataStorage),
-                  "Size of user data type 'T_User' is too large for allocated storage");
-    static_assert(alignof(T_User) <= alignof(UserDataStorage),
-                  "Alignment of user data type 'T_User' is larger than allocated storage");
-    T_User* const storagep = reinterpret_cast<T_User*>(&m_userDataStorage);
-    const uint32_t userCurrent = m_graphp->m_userCurrent;
-    UDEBUGONLY(UASSERT_OBJ(userCurrent, this, "DfgVertex user data used without reserving"););
-    if (m_userCnt != userCurrent) {
-        m_userCnt = userCurrent;
-        new (storagep) T_User{};
-    }
-    return *storagep;
-}
-
-template <typename T_User>
-const T_User& DfgVertex::getUser() const {
-    static_assert(sizeof(T_User) <= sizeof(UserDataStorage),
-                  "Size of user data type 'T_User' is too large for allocated storage");
-    static_assert(alignof(T_User) <= alignof(UserDataStorage),
-                  "Alignment of user data type 'T_User' is larger than allocated storage");
-    const T_User* const storagep = reinterpret_cast<const T_User*>(&m_userDataStorage);
-#if VL_DEBUG
-    const uint32_t userCurrent = m_graphp->m_userCurrent;
-    UASSERT_OBJ(userCurrent, this, "DfgVertex user data used without reserving");
-    UASSERT_OBJ(m_userCnt == userCurrent, this, "DfgVertex user data is stale");
-#endif
-    return *storagep;
-}
-
-template <typename T_User>
-T_User& DfgVertex::getUser() {
-    return const_cast<T_User&>(const_cast<const DfgVertex*>(this)->getUser<T_User>());
-}
-
-template <typename T_User>
-void DfgVertex::setUser(T_User value) {
-    static_assert(sizeof(T_User) <= sizeof(UserDataStorage),
-                  "Size of user data type 'T_User' is too large for allocated storage");
-    static_assert(alignof(T_User) <= alignof(UserDataStorage),
-                  "Alignment of user data type 'T_User' is larger than allocated storage");
-    T_User* const storagep = reinterpret_cast<T_User*>(&m_userDataStorage);
-    const uint32_t userCurrent = m_graphp->m_userCurrent;
-#if VL_DEBUG
-    UASSERT_OBJ(userCurrent, this, "DfgVertex user data used without reserving");
-#endif
-    m_userCnt = userCurrent;
-    *storagep = value;
+DfgUserMap<T_User> DfgGraph::makeUserMap() const {
+    return DfgUserMap<T_User>{this};
 }
 
 // }}}
diff --git a/src/V3DfgBreakCycles.cpp b/src/V3DfgBreakCycles.cpp
index c2b35a92b50..4a0ff65269f 100644
--- a/src/V3DfgBreakCycles.cpp
+++ b/src/V3DfgBreakCycles.cpp
@@ -29,6 +29,9 @@
 
 VL_DEFINE_DEBUG_FUNCTIONS;
 
+// Throughout these algorithm, we use the DfgUserMap as a map to the SCC number
+using Vtx2Scc = DfgUserMap<uint64_t>;
+
 class TraceDriver final : public DfgVisitor {
     // TYPES
 
@@ -63,6 +66,7 @@ class TraceDriver final : public DfgVisitor {
 
     // STATE
     DfgGraph& m_dfg;  // The graph being processed
+    Vtx2Scc& m_vtx2Scc;  // The Vertex to SCC map
     // The strongly connected component we are trying to escape
     const uint64_t m_component;
     const bool m_aggressive;  // Trace aggressively, creating intermediate ops
@@ -110,7 +114,7 @@ class TraceDriver final : public DfgVisitor {
 
         if VL_CONSTEXPR_CXX17 (std::is_same<DfgConst, Vertex>::value) {
             DfgConst* const vtxp = new DfgConst{m_dfg, refp->fileline(), width, 0};
-            vtxp->template setUser<uint64_t>(0);
+            m_vtx2Scc[vtxp] = 0;
             m_newVtxps.emplace_back(vtxp);
             return reinterpret_cast<Vertex*>(vtxp);
         } else {
@@ -121,7 +125,7 @@ class TraceDriver final : public DfgVisitor {
                                                   Vertex>::type;
             AstNodeDType* const dtypep = V3Dfg::dtypePacked(width);
             Vtx* const vtxp = new Vtx{m_dfg, refp->fileline(), dtypep};
-            vtxp->template setUser<uint64_t>(0);
+            m_vtx2Scc[vtxp] = 0;
             m_newVtxps.emplace_back(vtxp);
             return reinterpret_cast<Vertex*>(vtxp);
         }
@@ -163,7 +167,7 @@ class TraceDriver final : public DfgVisitor {
         // found what we were looking for. However, keep going past a splice,
         // or a unit as they cannot be use directly (they must always feed into
         // a variable, so we can't make them drive arbitrary logic)
-        if (vtxp->getUser<uint64_t>() != m_component  //
+        if (m_vtx2Scc[vtxp] != m_component  //
             && !vtxp->is<DfgVertexSplice>()  //
             && !vtxp->is<DfgUnitArray>()) {
             if (msb != vtxp->width() - 1 || lsb != 0) {
@@ -614,8 +618,9 @@ class TraceDriver final : public DfgVisitor {
 #undef SET_RESULT
 
     // CONSTRUCTOR
-    TraceDriver(DfgGraph& dfg, uint64_t component, bool aggressive)
+    TraceDriver(DfgGraph& dfg, Vtx2Scc& vtx2Scc, uint64_t component, bool aggressive)
         : m_dfg{dfg}
+        , m_vtx2Scc{vtx2Scc}
         , m_component{component}
         , m_aggressive{aggressive} {
 #ifdef VL_DEBUG
@@ -629,18 +634,18 @@ class TraceDriver final : public DfgVisitor {
     }
 
 public:
-    // Given a Vertex that is part of an SCC denoted by vtxp->user<uint64_t>(),
+    // Given a Vertex that is part of an SCC denoted by vtx2Scc,
     // return a vertex that is equivalent to 'vtxp[lsb +: width]', but is not
     // part of the same SCC. Returns nullptr if such a vertex cannot be
     // computed. This can add new vertices to the graph. The 'aggressive' flag
     // enables creating many intermediate operations. This should only be set
     // if it is reasonably certain the tracing will succeed, otherwise we can
     // waste a lot of compute.
-    static DfgVertex* apply(DfgGraph& dfg, DfgVertex* vtxp, uint32_t lsb, uint32_t width,
-                            bool aggressive) {
-        TraceDriver traceDriver{dfg, vtxp->getUser<uint64_t>(), aggressive};
+    static DfgVertex* apply(DfgGraph& dfg, Vtx2Scc& vtx2Scc, DfgVertex& vtx, uint32_t lsb,
+                            uint32_t width, bool aggressive) {
+        TraceDriver traceDriver{dfg, vtx2Scc, vtx2Scc[vtx], aggressive};
         // Find the out-of-component driver of the given vertex
-        DfgVertex* const resultp = traceDriver.trace(vtxp, lsb + width - 1, lsb);
+        DfgVertex* const resultp = traceDriver.trace(&vtx, lsb + width - 1, lsb);
         // Delete unused newly created vertices (these can be created if a
         // partial trace succeded, but an eventual one falied). Because new
         // vertices should be created depth first, it is enough to do a single
@@ -660,6 +665,7 @@ class TraceDriver final : public DfgVisitor {
 
 class IndependentBits final : public DfgVisitor {
     // STATE
+    const Vtx2Scc& m_vtx2Scc;  // The Vertex to SCC map
     const uint64_t m_component;  // The component the start vertex is part of
     // Vertex to current bit mask map. The mask is set for the bits that **depend** on 'm_varp'.
     std::unordered_map<const DfgVertex*, V3Number> m_vtxp2Mask;
@@ -675,9 +681,7 @@ class IndependentBits final : public DfgVisitor {
             std::forward_as_tuple(vtxp),  //
             std::forward_as_tuple(vtxp->fileline(), static_cast<int>(vtxp->width()), 0));
         // Initialize to all ones if the vertex is part of the same component, otherwise zeroes
-        if (pair.second && vtxp->getUser<uint64_t>() == m_component) {
-            pair.first->second.setAllBits1();
-        }
+        if (pair.second && m_vtx2Scc.at(vtxp) == m_component) { pair.first->second.setAllBits1(); }
         return pair.first->second;
     }
 
@@ -910,8 +914,9 @@ class IndependentBits final : public DfgVisitor {
 #undef MASK
 
     // CONSTRUCTOR
-    IndependentBits(DfgGraph& dfg, DfgVertex* vtxp)
-        : m_component{vtxp->getUser<uint64_t>()} {
+    IndependentBits(DfgGraph& dfg, const Vtx2Scc& vtx2Scc, DfgVertex& vertex)
+        : m_vtx2Scc{vtx2Scc}
+        , m_component{m_vtx2Scc.at(vertex)} {
 
 #ifdef VL_DEBUG
         if (v3Global.opt.debugCheck()) {
@@ -927,7 +932,7 @@ class IndependentBits final : public DfgVisitor {
 
         // Enqueue every operation vertex in the analysed component
         for (DfgVertex& vtx : dfg.opVertices()) {
-            if (vtx.getUser<uint64_t>() == m_component) workList.emplace_back(&vtx);
+            if (m_vtx2Scc.at(vtx) == m_component) workList.emplace_back(&vtx);
         }
 
         // While there is an item on the worklist ...
@@ -940,7 +945,7 @@ class IndependentBits final : public DfgVisitor {
                 // For an unpacked array vertex, just enque it's sinks.
                 // (There can be no loops through arrays directly)
                 currp->foreachSink([&](DfgVertex& vtx) {
-                    if (vtx.getUser<uint64_t>() == m_component) workList.emplace_back(&vtx);
+                    if (m_vtx2Scc.at(vtx) == m_component) workList.emplace_back(&vtx);
                     return false;
                 });
                 continue;
@@ -957,7 +962,7 @@ class IndependentBits final : public DfgVisitor {
             // If mask changed, enqueue sinks
             if (!prevMask.isCaseEq(maskCurr)) {
                 currp->foreachSink([&](DfgVertex& vtx) {
-                    if (vtx.getUser<uint64_t>() == m_component) workList.emplace_back(&vtx);
+                    if (m_vtx2Scc.at(vtx) == m_component) workList.emplace_back(&vtx);
                     return false;
                 });
 
@@ -981,62 +986,78 @@ class IndependentBits final : public DfgVisitor {
     // independent of the vertex itself (logic is not circular). The result is
     // a conservative estimate, so bits reported dependent might not actually
     // be, but all bits reported independent are known to be so.
-    static V3Number apply(DfgGraph& dfg, DfgVertex* vtxp) {
-        IndependentBits independentBits{dfg, vtxp};
+    static V3Number apply(DfgGraph& dfg, const Vtx2Scc& vtx2Scc, DfgVertex& vtx) {
+        IndependentBits independentBits{dfg, vtx2Scc, vtx};
         // The mask represents the dependent bits, so invert it
-        V3Number result{vtxp->fileline(), static_cast<int>(vtxp->width()), 0};
-        result.opNot(independentBits.mask(vtxp));
+        V3Number result{vtx.fileline(), static_cast<int>(vtx.width()), 0};
+        result.opNot(independentBits.mask(&vtx));
         return result;
     }
 };
 
 class FixUpSelDrivers final {
-    static size_t fixUpSelSinks(DfgGraph& dfg, DfgVertex* vtxp) {
-        size_t nImprovements = 0;
-        const uint64_t component = vtxp->getUser<uint64_t>();
-        vtxp->foreachSink([&](DfgVertex& sink) {
+    DfgGraph& m_dfg;  // The graph being processed
+    Vtx2Scc& m_vtx2Scc;  // The Vertex to SCC map
+    size_t m_nImprovements = 0;  // Number of improvements mde
+
+    void fixUpSelSinks(DfgVertex& vtx) {
+        const uint64_t component = m_vtx2Scc[vtx];
+        vtx.foreachSink([&](DfgVertex& sink) {
             // Ignore if sink is not part of same cycle
-            if (sink.getUser<uint64_t>() != component) return false;
+            if (m_vtx2Scc[sink] != component) return false;
             // Only handle Sel
             DfgSel* const selp = sink.cast<DfgSel>();
             if (!selp) return false;
             // Try to find the driver of the selected bits outside the cycle
             DfgVertex* const fixp
-                = TraceDriver::apply(dfg, vtxp, selp->lsb(), selp->width(), false);
+                = TraceDriver::apply(m_dfg, m_vtx2Scc, vtx, selp->lsb(), selp->width(), false);
             if (!fixp) return false;
             // Found an out-of-cycle driver. We can replace this sel with that.
             selp->replaceWith(fixp);
-            selp->unlinkDelete(dfg);
-            ++nImprovements;
+            selp->unlinkDelete(m_dfg);
+            ++m_nImprovements;
             return false;
         });
-        return nImprovements;
     }
 
-    static size_t fixUpArraySelSinks(DfgGraph& dfg, DfgVertex* vtxp) {
-        size_t nImprovements = 0;
-        const uint64_t component = vtxp->getUser<uint64_t>();
-        vtxp->foreachSink([&](DfgVertex& sink) {
+    void fixUpArraySelSinks(DfgVertex& vtx) {
+        const uint64_t component = m_vtx2Scc[vtx];
+        vtx.foreachSink([&](DfgVertex& sink) {
             // Ignore if sink is not part of same cycle
-            if (sink.getUser<uint64_t>() != component) return false;
+            if (m_vtx2Scc[sink] != component) return false;
             // Only handle ArraySels
-            DfgArraySel* const aselp = sink.cast<DfgArraySel>();
-            if (!aselp) return false;
+            DfgArraySel* const asp = sink.cast<DfgArraySel>();
+            if (!asp) return false;
             // First, try to fix up the whole word
-            if (DfgVertex* const fixp = TraceDriver::apply(dfg, aselp, 0, aselp->width(), false)) {
+            DfgVertex* const fixp
+                = TraceDriver::apply(m_dfg, m_vtx2Scc, *asp, 0, asp->width(), false);
+            if (fixp) {
                 // Found an out-of-cycle driver for the whole ArraySel
-                aselp->replaceWith(fixp);
-                aselp->unlinkDelete(dfg);
-                ++nImprovements;
+                asp->replaceWith(fixp);
+                asp->unlinkDelete(m_dfg);
+                ++m_nImprovements;
                 return false;
             }
             // Attempt to fix up piece-wise at Sels applied to the ArraySel
-            nImprovements += fixUpSelSinks(dfg, aselp);
+            fixUpSelSinks(*asp);
             // Remove if became unused
-            if (!aselp->hasSinks()) aselp->unlinkDelete(dfg);
+            if (!asp->hasSinks()) asp->unlinkDelete(m_dfg);
             return false;
         });
-        return nImprovements;
+    }
+
+    FixUpSelDrivers(DfgGraph& dfg, Vtx2Scc& vtx2Scc, DfgVertexVar& var)
+        : m_dfg{dfg}
+        , m_vtx2Scc{vtx2Scc} {
+        UINFO(9, "FixUpSelDrivers of " << var.nodep()->name());
+        if (var.isPacked()) {
+            // For Packed variables, fix up all Sels applied to it
+            fixUpSelSinks(var);
+        } else if (var.isArray()) {
+            // For Array variables, fix up each ArraySel applied to it
+            fixUpArraySelSinks(var);
+        }
+        UINFO(9, "FixUpSelDrivers made " << m_nImprovements << " improvements");
     }
 
 public:
@@ -1044,26 +1065,20 @@ class FixUpSelDrivers final {
     // expression of the selected bits. This can fix things like
     // 'a[1:0] = foo', 'a[2] = a[1]', which are somewhat common.
     // Returns the number of drivers fixed up.
-    static size_t apply(DfgGraph& dfg, DfgVertexVar* varp) {
-        UINFO(9, "FixUpSelDrivers of " << varp->nodep()->name());
-        size_t nImprovements = 0;
-        if (varp->is<DfgVarPacked>()) {
-            // For Packed variables, fix up all Sels applied to it
-            nImprovements += fixUpSelSinks(dfg, varp);
-        } else if (varp->is<DfgVarArray>()) {
-            // For Array variables, fix up each ArraySel applied to it
-            nImprovements += fixUpArraySelSinks(dfg, varp);
-        }
-        UINFO(9, "FixUpSelDrivers made " << nImprovements << " improvements");
-        return nImprovements;
+    static size_t apply(DfgGraph& dfg, Vtx2Scc& vtx2Scc, DfgVertexVar& var) {
+        return FixUpSelDrivers{dfg, vtx2Scc, var}.m_nImprovements;
     }
 };
 
 class FixUpIndependentRanges final {
-    // Returns a bitmask set if that bit of 'vtxp' is used (has a sink)
-    static V3Number computeUsedBits(DfgVertex* vtxp) {
-        V3Number result{vtxp->fileline(), static_cast<int>(vtxp->width()), 0};
-        vtxp->foreachSink([&result](DfgVertex& sink) {
+    DfgGraph& m_dfg;  // The graph being processed
+    Vtx2Scc& m_vtx2Scc;  // The Vertex to SCC map
+    size_t m_nImprovements = 0;  // Number of improvements mde
+
+    // Returns a bitmask set if that bit of 'vtx' is used (has a sink)
+    static V3Number computeUsedBits(DfgVertex& vtx) {
+        V3Number result{vtx.fileline(), static_cast<int>(vtx.width()), 0};
+        vtx.foreachSink([&result](DfgVertex& sink) {
             // If used via a Sel, mark the selected bits used
             if (const DfgSel* const selp = sink.cast<DfgSel>()) {
                 uint32_t lsb = selp->lsb();
@@ -1078,24 +1093,22 @@ class FixUpIndependentRanges final {
         return result;
     }
 
-    static std::string debugStr(const DfgVertex* vtxp) {
-        if (const DfgArraySel* const aselp = vtxp->cast<DfgArraySel>()) {
+    static std::string debugStr(const DfgVertex& vtx) {
+        if (const DfgArraySel* const aselp = vtx.cast<DfgArraySel>()) {
             const size_t i = aselp->bitp()->as<DfgConst>()->toSizeT();
-            return debugStr(aselp->fromp()) + "[" + std::to_string(i) + "]";
+            return debugStr(*aselp->fromp()) + "[" + std::to_string(i) + "]";
         }
-        if (const DfgVertexVar* const varp = vtxp->cast<DfgVertexVar>()) {
+        if (const DfgVertexVar* const varp = vtx.cast<DfgVertexVar>()) {
             return varp->nodep()->name();
         }
-        vtxp->v3fatalSrc("Unhandled node type");  // LCOV_EXCL_LINE
+        vtx.v3fatalSrc("Unhandled node type");  // LCOV_EXCL_LINE
     }
 
     // Trace drivers of independent bits of 'vtxp' in the range '[hi:lo]'
     // append replacement terms to 'termps'. Returns number of successful
     // replacements.
-    static size_t gatherTerms(std::vector<DfgVertex*>& termps, DfgGraph& dfg,
-                              DfgVertex* const vtxp, const V3Number& indpBits, const uint32_t hi,
-                              const uint32_t lo) {
-        size_t nImprovements = 0;
+    void gatherTerms(std::vector<DfgVertex*>& termps, DfgVertex& vtx, const V3Number& indpBits,
+                     const uint32_t hi, const uint32_t lo) {
         // Iterate through consecutive dependent/non-dependet ranges within [hi:lo]
         bool isIndependent = indpBits.bitIs1(lo);
         uint32_t lsb = lo;
@@ -1106,20 +1119,20 @@ class FixUpIndependentRanges final {
             DfgVertex* termp = nullptr;
             // If the range is not self-dependent, attempt to trace its driver
             if (isIndependent) {
-                termp = TraceDriver::apply(dfg, vtxp, lsb, width, true);
+                termp = TraceDriver::apply(m_dfg, m_vtx2Scc, vtx, lsb, width, true);
                 if (termp) {
-                    ++nImprovements;
+                    ++m_nImprovements;
                 } else {
                     UINFO(5, "TraceDriver of independent range failed for "
-                                 << debugStr(vtxp) << "[" << msb << ":" << lsb << "]");
+                                 << debugStr(vtx) << "[" << msb << ":" << lsb << "]");
                 }
             }
             // Fall back on using the part of the variable (if dependent, or trace failed)
             if (!termp) {
                 AstNodeDType* const dtypep = V3Dfg::dtypePacked(width);
-                DfgSel* const selp = new DfgSel{dfg, vtxp->fileline(), dtypep};
+                DfgSel* const selp = new DfgSel{m_dfg, vtx.fileline(), dtypep};
                 // Same component as 'vtxp', as reads 'vtxp' and will replace 'vtxp'
-                selp->setUser<uint64_t>(vtxp->getUser<uint64_t>());
+                m_vtx2Scc[selp] = m_vtx2Scc.at(vtx);
                 // Do not connect selp->fromp yet, need to do afer replacing 'vtxp'
                 selp->lsb(lsb);
                 termp = selp;
@@ -1130,40 +1143,38 @@ class FixUpIndependentRanges final {
             isIndependent = !isIndependent;
             lsb = msb + 1;
         }
-        return nImprovements;
     }
 
-    static size_t fixUpPacked(DfgGraph& dfg, DfgVertex* vtxp) {
-        UASSERT_OBJ(VN_IS(vtxp->dtypep(), BasicDType), vtxp, "Should be a packed BasicDType");
-        size_t nImprovements = 0;
+    void fixUpPacked(DfgVertex& vtx) {
+        UASSERT_OBJ(vtx.isPacked(), &vtx, "Should be a packed type");
 
         // Figure out which bits of 'vtxp' are used
-        const V3Number usedBits = computeUsedBits(vtxp);
-        UINFO(9, "Used        bits of '" << debugStr(vtxp) << "' are "
-                                         << usedBits.displayed(vtxp->fileline(), "%b"));
+        const V3Number usedBits = computeUsedBits(vtx);
+        UINFO(9, "Used        bits of '" << debugStr(vtx) << "' are "
+                                         << usedBits.displayed(vtx.fileline(), "%b"));
         // Nothing to do if no bits are used
-        if (usedBits.isEqZero()) return 0;
+        if (usedBits.isEqZero()) return;
 
         // Figure out which bits of 'vtxp' are dependent of themselves
-        const V3Number indpBits = IndependentBits::apply(dfg, vtxp);
-        UINFO(9, "Independent bits of '" << debugStr(vtxp) << "' are "
-                                         << indpBits.displayed(vtxp->fileline(), "%b"));
+        const V3Number indpBits = IndependentBits::apply(m_dfg, m_vtx2Scc, vtx);
+        UINFO(9, "Independent bits of '" << debugStr(vtx) << "' are "
+                                         << indpBits.displayed(vtx.fileline(), "%b"));
         // Can't do anything if all bits are dependent
-        if (indpBits.isEqZero()) return 0;
+        if (indpBits.isEqZero()) return;
 
         {
             // Nothing to do if no used bits are independen (all used bits are dependent)
-            V3Number usedAndIndependent{vtxp->fileline(), static_cast<int>(vtxp->width()), 0};
+            V3Number usedAndIndependent{vtx.fileline(), static_cast<int>(vtx.width()), 0};
             usedAndIndependent.opAnd(usedBits, indpBits);
-            if (usedAndIndependent.isEqZero()) return 0;
+            if (usedAndIndependent.isEqZero()) return;
         }
 
         // We are computing the terms to concatenate and replace 'vtxp' with
         std::vector<DfgVertex*> termps;
 
         // Iterate through consecutive used/unused ranges
-        FileLine* const flp = vtxp->fileline();
-        const uint32_t width = vtxp->width();
+        FileLine* const flp = vtx.fileline();
+        const uint32_t width = vtx.width();
         bool isUsed = usedBits.bitIs1(0);  // Is current range used
         uint32_t lsb = 0;  // LSB of current range
         for (uint32_t msb = 0; msb < width; ++msb) {
@@ -1171,11 +1182,11 @@ class FixUpIndependentRanges final {
             if (!endRange) continue;
             if (isUsed) {
                 // The range is used, compute the replacement terms
-                nImprovements += gatherTerms(termps, dfg, vtxp, indpBits, msb, lsb);
+                gatherTerms(termps, vtx, indpBits, msb, lsb);
             } else {
                 // The range is not used, just use constant 0 as a placeholder
-                DfgConst* const constp = new DfgConst{dfg, flp, msb - lsb + 1, 0};
-                constp->setUser<uint64_t>(0);
+                DfgConst* const constp = new DfgConst{m_dfg, flp, msb - lsb + 1, 0};
+                m_vtx2Scc[constp] = 0;
                 termps.emplace_back(constp);
             }
             // Next iteration
@@ -1184,21 +1195,21 @@ class FixUpIndependentRanges final {
         }
 
         // If no imporovement was possible, delete the terms we just created
-        if (!nImprovements) {
-            for (DfgVertex* const termp : termps) VL_DO_DANGLING(termp->unlinkDelete(dfg), termp);
+        if (!m_nImprovements) {
+            for (DfgVertex* const tp : termps) VL_DO_DANGLING(tp->unlinkDelete(m_dfg), tp);
             termps.clear();
-            return 0;
+            return;
         }
 
         // We managed to imporove something, apply the replacement
         // Concatenate all the terms to create the replacement
         DfgVertex* replacementp = termps.front();
-        const uint64_t vComp = vtxp->getUser<uint64_t>();
+        const uint64_t vComp = m_vtx2Scc.at(vtx);
         for (size_t i = 1; i < termps.size(); ++i) {
             DfgVertex* const termp = termps[i];
             const uint32_t catWidth = replacementp->width() + termp->width();
             AstNodeDType* const dtypep = V3Dfg::dtypePacked(catWidth);
-            DfgConcat* const catp = new DfgConcat{dfg, flp, dtypep};
+            DfgConcat* const catp = new DfgConcat{m_dfg, flp, dtypep};
             catp->rhsp(replacementp);
             catp->lhsp(termp);
             // Need to figure out which component the replacement vertex
@@ -1206,56 +1217,61 @@ class FixUpIndependentRanges final {
             // component, it's part of that component, otherwise its not
             // cyclic (all terms are from outside the original component,
             // and feed into the original component).
-            const uint64_t tComp = termp->getUser<uint64_t>();
-            const uint64_t rComp = replacementp->getUser<uint64_t>();
-            catp->setUser<uint64_t>(tComp == vComp || rComp == vComp ? vComp : 0);
+            const uint64_t tComp = m_vtx2Scc.at(termp);
+            const uint64_t rComp = m_vtx2Scc.at(replacementp);
+            m_vtx2Scc[catp] = tComp == vComp || rComp == vComp ? vComp : 0;
             replacementp = catp;
         }
 
         // Replace the vertex
-        vtxp->replaceWith(replacementp);
+        vtx.replaceWith(replacementp);
         // Connect the Sel nodes in the replacement
         for (DfgVertex* const termp : termps) {
             if (DfgSel* const selp = termp->cast<DfgSel>()) {
-                if (!selp->fromp()) selp->fromp(vtxp);
+                if (!selp->fromp()) selp->fromp(&vtx);
             }
         }
-
-        // Done
-        return nImprovements;
     }
 
-public:
-    // Similar to FixUpSelDrivers, but first comptute which bits of the
-    // variable are self dependent, and fix up those that are independent
-    // but used.
-    static size_t apply(DfgGraph& dfg, DfgVertexVar* varp) {
-        UINFO(9, "FixUpIndependentRanges of " << varp->nodep()->name());
-        size_t nImprovements = 0;
+    void main(DfgVertexVar& var) {
+        UINFO(9, "FixUpIndependentRanges of " << var.nodep()->name());
 
-        if (varp->is<DfgVarPacked>()) {
+        if (var.is<DfgVarPacked>()) {
             // For Packed variables, fix up as whole
-            nImprovements += fixUpPacked(dfg, varp);
-        } else if (varp->is<DfgVarArray>()) {
+            fixUpPacked(var);
+        } else if (var.is<DfgVarArray>()) {
             // For array variables, fix up element-wise
-            const uint64_t component = varp->getUser<uint64_t>();
-            varp->foreachSink([&](DfgVertex& sink) {
+            const uint64_t component = m_vtx2Scc.at(var);
+            var.foreachSink([&](DfgVertex& sink) {
                 // Ignore if sink is not part of same cycle
-                if (sink.getUser<uint64_t>() != component) return false;
+                if (m_vtx2Scc.at(sink) != component) return false;
                 // Only handle ArraySels with constant index
                 DfgArraySel* const aselp = sink.cast<DfgArraySel>();
                 if (!aselp) return false;
                 if (!aselp->bitp()->is<DfgConst>()) return false;
                 // Fix up the word
-                nImprovements += fixUpPacked(dfg, aselp);
+                fixUpPacked(*aselp);
                 // Remove if became unused
-                if (!aselp->hasSinks()) aselp->unlinkDelete(dfg);
+                if (!aselp->hasSinks()) aselp->unlinkDelete(m_dfg);
                 return false;
             });
         }
 
-        UINFO(9, "FixUpIndependentRanges made " << nImprovements << " improvements");
-        return nImprovements;
+        UINFO(9, "FixUpIndependentRanges made " << m_nImprovements << " improvements");
+    }
+
+    FixUpIndependentRanges(DfgGraph& dfg, Vtx2Scc& vtx2Scc, DfgVertexVar& var)
+        : m_dfg{dfg}
+        , m_vtx2Scc{vtx2Scc} {
+        main(var);
+    }
+
+public:
+    // Similar to FixUpSelDrivers, but first comptute which bits of the
+    // variable are self dependent, and fix up those that are independent
+    // but used.
+    static size_t apply(DfgGraph& dfg, Vtx2Scc& vtx2Scc, DfgVertexVar& var) {
+        return FixUpIndependentRanges{dfg, vtx2Scc, var}.m_nImprovements;
     }
 };
 
@@ -1292,8 +1308,9 @@ V3DfgPasses::breakCycles(const DfgGraph& dfg, V3DfgContext& ctx) {
     // Iterate while an improvement can be made and the graph is still cyclic
     do {
         // Color SCCs (populates DfgVertex::user<uint64_t>())
-        const auto userDataInUse = res.userDataInUse();
-        const uint32_t numNonTrivialSCCs = V3DfgPasses::colorStronglyConnectedComponents(res);
+        Vtx2Scc vtx2Scc = res.makeUserMap<uint64_t>();
+        const uint32_t numNonTrivialSCCs
+            = V3DfgPasses::colorStronglyConnectedComponents(res, vtx2Scc);
 
         // Congrats if it has become acyclic
         if (!numNonTrivialSCCs) {
@@ -1310,10 +1327,9 @@ V3DfgPasses::breakCycles(const DfgGraph& dfg, V3DfgContext& ctx) {
         // Method 1: FixUpSelDrivers
         for (DfgVertexVar& vtx : res.varVertices()) {
             // If Variable is not part of a cycle, move on
-            const uint64_t component = vtx.getUser<uint64_t>();
-            if (!component) continue;
+            if (!vtx2Scc[vtx]) continue;
 
-            const size_t nFixed = FixUpSelDrivers::apply(res, &vtx);
+            const size_t nFixed = FixUpSelDrivers::apply(res, vtx2Scc, vtx);
             if (nFixed) {
                 nImprovements += nFixed;
                 ctx.m_breakCyclesContext.m_nImprovements += nFixed;
@@ -1327,10 +1343,9 @@ V3DfgPasses::breakCycles(const DfgGraph& dfg, V3DfgContext& ctx) {
         // Method 2. FixUpIndependentRanges
         for (DfgVertexVar& vtx : res.varVertices()) {
             // If Variable is not part of a cycle, move on
-            const uint64_t component = vtx.getUser<uint64_t>();
-            if (!component) continue;
+            if (!vtx2Scc[vtx]) continue;
 
-            const size_t nFixed = FixUpIndependentRanges::apply(res, &vtx);
+            const size_t nFixed = FixUpIndependentRanges::apply(res, vtx2Scc, vtx);
             if (nFixed) {
                 nImprovements += nFixed;
                 ctx.m_breakCyclesContext.m_nImprovements += nFixed;
@@ -1340,10 +1355,11 @@ V3DfgPasses::breakCycles(const DfgGraph& dfg, V3DfgContext& ctx) {
     } while (nImprovements != prevNImprovements);
 
     if (dumpDfgLevel() >= 9) {
-        const auto userDataInUse = res.userDataInUse();
-        V3DfgPasses::colorStronglyConnectedComponents(res);
-        res.dumpDotFilePrefixed(ctx.prefix() + "breakCycles-remaining",
-                                [](const DfgVertex& vtx) { return vtx.getUser<uint64_t>(); });
+        Vtx2Scc vtx2Scc = res.makeUserMap<uint64_t>();
+        V3DfgPasses::colorStronglyConnectedComponents(res, vtx2Scc);
+        res.dumpDotFilePrefixed(ctx.prefix() + "breakCycles-remaining", [&](const DfgVertex& vtx) {
+            return vtx2Scc[vtx];  //
+        });
     }
 
     // If an improvement was made, return the still cyclic improved graph
diff --git a/src/V3DfgColorSCCs.cpp b/src/V3DfgColorSCCs.cpp
index 6caa0b957ad..3d02e1bd32c 100644
--- a/src/V3DfgColorSCCs.cpp
+++ b/src/V3DfgColorSCCs.cpp
@@ -26,52 +26,51 @@
 #include <limits>
 #include <vector>
 
-// Similar algorithm used in ExtractCyclicComponents.
-// This one sets DfgVertex::user(). See the static 'apply' method below.
 class ColorStronglyConnectedComponents final {
-    static constexpr uint32_t UNASSIGNED = std::numeric_limits<uint32_t>::max();
+    static_assert(sizeof(uint32_t[2]) == sizeof(uint64_t), "Incorrect ovverlay size");
 
-    // TYPES
-    struct VertexState final {
-        uint32_t component = UNASSIGNED;  // Result component number (0 means not in SCC)
-        uint32_t index = UNASSIGNED;  // Used by Pearce's algorithm for detecting SCCs
-        VertexState() = default;
-        VertexState(uint32_t i, uint32_t n)
-            : component{n}
-            , index{i} {}
-    };
+    // CONSTANTS
+    static constexpr uint32_t UNASSIGNED = std::numeric_limits<uint32_t>::max();
 
     // STATE
-    DfgGraph& m_dfg;  // The input graph
+    const DfgGraph& m_dfg;  // The input graph
+    DfgUserMap<uint64_t>& m_map;  // The result map we are computing - also used for traversal
     uint32_t m_nonTrivialSCCs = 0;  // Number of non-trivial SCCs in the graph
     uint32_t m_index = 0;  // Visitation index counter
-    std::vector<DfgVertex*> m_stack;  // The stack used by the algorithm
+    std::vector<const DfgVertex*> m_stack;  // The stack used by the algorithm
 
     // METHODS
-    void visitColorSCCs(DfgVertex& vtx, VertexState& vtxState) {
-        UDEBUGONLY(UASSERT_OBJ(vtxState.index == UNASSIGNED, &vtx, "Already visited vertex"););
+    // Use the bottom 32-bit word as the component number
+    uint32_t& component(const DfgVertex& vtx) {  //
+        return reinterpret_cast<uint32_t(&)[2]>(m_map[vtx])[0];
+    }
+    // Use the top 32-bit word as the visitation index
+    uint32_t& index(const DfgVertex& vtx) {  //
+        return reinterpret_cast<uint32_t(&)[2]>(m_map[vtx])[1];
+    }
+
+    void visitColorSCCs(const DfgVertex& vtx) {
+        UDEBUGONLY(UASSERT_OBJ(index(vtx) == UNASSIGNED, &vtx, "Already visited vertex"););
 
         // Visiting vertex
-        const size_t rootIndex = vtxState.index = ++m_index;
+        const size_t rootIndex = index(vtx) = ++m_index;
 
         // Visit children
-        vtx.foreachSink([&](DfgVertex& child) {
-            VertexState& childSatate = child.user<VertexState>();
+        vtx.foreachSink([&](const DfgVertex& child) {
             // If the child has not yet been visited, then continue traversal
-            if (childSatate.index == UNASSIGNED) visitColorSCCs(child, childSatate);
+            if (index(child) == UNASSIGNED) visitColorSCCs(child);
             // If the child is not in an SCC
-            if (childSatate.component == UNASSIGNED) {
-                if (vtxState.index > childSatate.index) vtxState.index = childSatate.index;
+            if (component(child) == UNASSIGNED) {
+                if (index(vtx) > index(child)) index(vtx) = index(child);
             }
             return false;
         });
 
-        if (vtxState.index == rootIndex) {
+        if (index(vtx) == rootIndex) {
             // This is the 'root' of an SCC
 
             // A trivial SCC contains only a single vertex
-            const bool isTrivial = m_stack.empty()  //
-                                   || m_stack.back()->getUser<VertexState>().index < rootIndex;
+            const bool isTrivial = m_stack.empty() || index(*m_stack.back()) < rootIndex;
             // We also need a separate component for vertices that drive themselves (which can
             // happen for input like 'assign a = a'), as we want to extract them (they are cyclic).
             const bool drivesSelf = vtx.foreachSink([&](const DfgVertex& sink) {  //
@@ -81,17 +80,16 @@ class ColorStronglyConnectedComponents final {
             if (!isTrivial || drivesSelf) {
                 // Allocate new component
                 ++m_nonTrivialSCCs;
-                vtxState.component = m_nonTrivialSCCs;
+                component(vtx) = m_nonTrivialSCCs;
                 while (!m_stack.empty()) {
-                    VertexState& topState = m_stack.back()->getUser<VertexState>();
                     // Only higher nodes belong to the same SCC
-                    if (topState.index < rootIndex) break;
+                    if (index(*m_stack.back()) < rootIndex) break;
+                    component(*m_stack.back()) = m_nonTrivialSCCs;
                     m_stack.pop_back();
-                    topState.component = m_nonTrivialSCCs;
                 }
             } else {
                 // Trivial SCC (and does not drive itself), so acyclic. Keep it in original graph.
-                vtxState.component = 0;
+                component(vtx) = 0;
             }
         } else {
             // Not the root of an SCC
@@ -102,56 +100,61 @@ class ColorStronglyConnectedComponents final {
     void colorSCCs() {
         // We know constant nodes have no input edges, so they cannot be part
         // of a non-trivial SCC. Mark them as such without any real traversals.
-        for (DfgConst& vtx : m_dfg.constVertices()) vtx.setUser(VertexState{0, 0});
+        for (const DfgConst& vtx : m_dfg.constVertices()) {
+            index(vtx) = 0;
+            component(vtx) = 0;
+        }
 
-        // Start traversals through variables
-        for (DfgVertexVar& vtx : m_dfg.varVertices()) {
-            VertexState& vtxState = vtx.user<VertexState>();
-            // If it has no input or no outputs, it cannot be part of a non-trivial SCC.
+        // Initialize state of variable vertices
+        for (const DfgVertexVar& vtx : m_dfg.varVertices()) {
+            // If it has no inputs or no outputs, it cannot be part of a non-trivial SCC.
             if ((!vtx.srcp() && !vtx.defaultp()) || !vtx.hasSinks()) {
-                UDEBUGONLY(UASSERT_OBJ(vtxState.index == UNASSIGNED || vtxState.component == 0,
-                                       &vtx, "Non circular variable must be in a trivial SCC"););
-                vtxState.index = 0;
-                vtxState.component = 0;
+                index(vtx) = 0;
+                component(vtx) = 0;
                 continue;
             }
-            // If not yet visited, start a traversal
-            if (vtxState.index == UNASSIGNED) visitColorSCCs(vtx, vtxState);
+            index(vtx) = UNASSIGNED;
+            component(vtx) = UNASSIGNED;
+        }
+
+        // Initialize state of operation vertices
+        for (const DfgVertex& vtx : m_dfg.opVertices()) {
+            index(vtx) = UNASSIGNED;
+            component(vtx) = UNASSIGNED;
+        }
+
+        // Start traversals through not yet visited variables
+        for (const DfgVertexVar& vtx : m_dfg.varVertices()) {
+            if (index(vtx) == UNASSIGNED) visitColorSCCs(vtx);
         }
 
-        // Start traversals through operations
-        for (DfgVertex& vtx : m_dfg.opVertices()) {
-            VertexState& vtxState = vtx.user<VertexState>();
-            // If not yet visited, start a traversal
-            if (vtxState.index == UNASSIGNED) visitColorSCCs(vtx, vtxState);
+        // Start traversals through not yet visited operations
+        for (const DfgVertex& vtx : m_dfg.opVertices()) {
+            if (index(vtx) == UNASSIGNED) visitColorSCCs(vtx);
         }
     }
 
-    explicit ColorStronglyConnectedComponents(DfgGraph& dfg)
-        : m_dfg{dfg} {
+    explicit ColorStronglyConnectedComponents(const DfgGraph& dfg, DfgUserMap<uint64_t>& map)
+        : m_dfg{dfg}
+        , m_map{map} {
         UASSERT(dfg.size() < UNASSIGNED, "Graph too big " << dfg.name());
         // Yet another implementation of Pearce's algorithm.
         colorSCCs();
-        // Re-assign user values
-        m_dfg.forEachVertex([](DfgVertex& vtx) {
-            const uint64_t component = vtx.getUser<VertexState>().component;
-            vtx.setUser<uint64_t>(component);
+        // Re-assign mapped values
+        m_dfg.forEachVertex([&](const DfgVertex& vtx) {
+            const uint64_t c = component(vtx);
+            map[vtx] = c;
         });
     }
 
 public:
-    // Sets DfgVertex::user<uint64_t>() for all vertext to:
-    // - 0, if the vertex is not part of a non-trivial strongly connected component
-    //   and is not part of a self-loop. That is: the Vertex is not part of any cycle.
-    // - N, if the vertex is part of a non-trivial strongly conneced component or self-loop N.
-    //   That is: each set of vertices that are reachable from each other will have the same
-    //   non-zero value assigned.
-    // Returns the number of non-trivial SCCs (~distinct cycles)
-    static uint32_t apply(DfgGraph& dfg) {
-        return ColorStronglyConnectedComponents{dfg}.m_nonTrivialSCCs;
+    // See declaration of  V3DfgPasses::colorStronglyConnectedComponents
+    static uint32_t apply(const DfgGraph& dfg, DfgUserMap<uint64_t>& map) {
+        return ColorStronglyConnectedComponents{dfg, map}.m_nonTrivialSCCs;
     }
 };
 
-uint32_t V3DfgPasses::colorStronglyConnectedComponents(DfgGraph& dfg) {
-    return ColorStronglyConnectedComponents::apply(dfg);
+uint32_t V3DfgPasses::colorStronglyConnectedComponents(const DfgGraph& dfg,
+                                                       DfgUserMap<uint64_t>& map) {
+    return ColorStronglyConnectedComponents::apply(dfg, map);
 }
diff --git a/src/V3DfgDecomposition.cpp b/src/V3DfgDecomposition.cpp
index c73d1b9799c..531762f8c4e 100644
--- a/src/V3DfgDecomposition.cpp
+++ b/src/V3DfgDecomposition.cpp
@@ -34,6 +34,8 @@ class SplitIntoComponents final {
 
     // STATE
     DfgGraph& m_dfg;  // The input graph
+    // Map from vertices to the weekly connected component they belong to
+    DfgUserMap<size_t> m_component = m_dfg.makeUserMap<size_t>();
     const std::string m_prefix;  // Component name prefix
     std::vector<std::unique_ptr<DfgGraph>> m_components;  // The extracted components
     // Component counter - starting from 1 as 0 is the default value used as a marker
@@ -44,10 +46,10 @@ class SplitIntoComponents final {
         std::vector<DfgVertex*> queue;
         queue.reserve(m_dfg.size());
 
-        // any sort of interesting logic must involve a variable, so we only need to iterate them
+        // Any sort of interesting logic must involve a variable, so we only need to iterate them
         for (DfgVertexVar& vtx : m_dfg.varVertices()) {
             // If already assigned this vertex to a component, then continue
-            if (vtx.user<size_t>()) continue;
+            if (m_component[vtx]) continue;
 
             // Start depth first traversal at this vertex
             queue.push_back(&vtx);
@@ -59,10 +61,10 @@ class SplitIntoComponents final {
                 queue.pop_back();
 
                 // Move on if already visited
-                if (item.user<size_t>()) continue;
+                if (m_component[item]) continue;
 
                 // Assign to current component
-                item.user<size_t>() = m_componentCounter;
+                m_component[item] = m_componentCounter;
 
                 // Enqueue all sources and sinks of this vertex.
                 item.foreachSource([&](DfgVertex& src) {
@@ -83,7 +85,7 @@ class SplitIntoComponents final {
     template <typename Vertex>
     void moveVertices(DfgVertex::List<Vertex>& list) {
         for (DfgVertex* const vtxp : list.unlinkable()) {
-            if (const size_t component = vtxp->user<size_t>()) {
+            if (const size_t component = m_component[vtxp]) {
                 m_dfg.removeVertex(*vtxp);
                 m_components[component - 1]->addVertex(*vtxp);
             } else {
@@ -96,8 +98,6 @@ class SplitIntoComponents final {
     SplitIntoComponents(DfgGraph& dfg, const std::string& label)
         : m_dfg{dfg}
         , m_prefix{dfg.name() + (label.empty() ? "" : "-") + label + "-component-"} {
-        // Component number is stored as DfgVertex::user<size_t>()
-        const auto userDataInUse = m_dfg.userDataInUse();
         // Color each component of the graph
         colorComponents();
         // Allocate the component graphs
@@ -126,6 +126,8 @@ std::vector<std::unique_ptr<DfgGraph>> DfgGraph::splitIntoComponents(const std::
 class ExtractCyclicComponents final {
     // STATE
     DfgGraph& m_dfg;  // The input graph
+    // Map from vertices to the component they belong to
+    DfgUserMap<uint64_t> m_component = m_dfg.makeUserMap<uint64_t>();
     const std::string m_prefix;  // Component name prefix
     const bool m_doExpensiveChecks = v3Global.opt.debugCheck();
     // The extracted cyclic components
@@ -137,19 +139,21 @@ class ExtractCyclicComponents final {
     void addVertexAndExpandSiblings(DfgVertex& vtx, uint64_t component) {
         // Do not go past a variable, we will partition the graph there
         if (vtx.is<DfgVertexVar>()) return;
+        // Pick up component value reference
+        uint64_t& vtxComponentr = m_component.at(vtx);
         // Don't need to recurse if the vertex is already in the same component,
         // it was either marked through an earlier traversal, in which case it
         // was processed recursively, or it will be processed later.
-        if (vtx.getUser<uint64_t>() == component) return;
+        if (vtxComponentr == component) return;
         // Because all cycles are through a variable, we can't reach another SCC.
-        UASSERT_OBJ(!vtx.getUser<uint64_t>(), &vtx, "Cycle without variable involvement");
+        UASSERT_OBJ(!vtxComponentr, &vtx, "Cycle without variable involvement");
         // Put this vertex in the component, and continue recursively
-        vtx.setUser<uint64_t>(component);
+        vtxComponentr = component;
         expandSiblings(vtx, component);
     }
 
     void expandSiblings(DfgVertex& vtx, uint64_t component) {
-        UASSERT_OBJ(vtx.getUser<uint64_t>() == component, &vtx, "Traversal didn't stop");
+        UASSERT_OBJ(m_component.at(vtx) == component, &vtx, "Traversal didn't stop");
         vtx.foreachSink([&](DfgVertex& v) {
             addVertexAndExpandSiblings(v, component);
             return false;
@@ -179,22 +183,22 @@ class ExtractCyclicComponents final {
         // component if they are not variables themselves. The assertions below
         // must hold because of the assumption above.
         for (DfgVertexVar& vtx : m_dfg.varVertices()) {
-            const uint64_t varComponent = vtx.getUser<uint64_t>();
+            const uint64_t varComponent = m_component.at(vtx);
             if (!varComponent) continue;
             if (DfgVertex* const srcp = vtx.srcp()) {
                 if (!srcp->is<DfgVertexVar>()) {
-                    const uint64_t srcComponent = srcp->getUser<uint64_t>();
+                    uint64_t& srcComponent = m_component.at(srcp);
                     UASSERT_OBJ(!srcComponent || srcComponent == varComponent, srcp,
                                 "Cycle through 'srcp' that does not go through variable.");
-                    srcp->setUser<uint64_t>(varComponent);
+                    srcComponent = varComponent;
                 }
             }
             if (DfgVertex* const defp = vtx.defaultp()) {
                 if (!defp->is<DfgVertexVar>()) {
-                    const uint64_t defComponent = defp->getUser<uint64_t>();
+                    uint64_t& defComponent = m_component.at(defp);
                     UASSERT_OBJ(!defComponent || defComponent == varComponent, defp,
                                 "Cycle through 'defaultp' that does not go through variable");
-                    defp->setUser<uint64_t>(varComponent);
+                    defComponent = varComponent;
                 }
             }
         }
@@ -205,7 +209,7 @@ class ExtractCyclicComponents final {
         // We do this by staring a DFS from each vertex that is part of an
         // component and add all reachable non-variable vertices to the same.
         for (DfgVertex& vtx : m_dfg.opVertices()) {
-            if (const uint64_t targetComponent = vtx.getUser<uint64_t>()) {
+            if (const uint64_t targetComponent = m_component.at(vtx)) {
                 expandSiblings(vtx, targetComponent);
             }
         }
@@ -213,7 +217,7 @@ class ExtractCyclicComponents final {
 
     // Retrieve clone of vertex in the given component
     DfgVertexVar* getClone(DfgVertexVar& vtx, uint64_t component) {
-        UASSERT_OBJ(vtx.getUser<uint64_t>() != component, &vtx, "Vertex is in that component");
+        UASSERT_OBJ(m_component.at(vtx) != component, &vtx, "Vertex is in that component");
         DfgVertexVar*& clonep = m_clones[&vtx][component];
         if (!clonep) {
             if (DfgVarPacked* const pVtxp = vtx.cast<DfgVarPacked>()) {
@@ -230,7 +234,7 @@ class ExtractCyclicComponents final {
                 }
             }
             UASSERT_OBJ(clonep, &vtx, "Unhandled 'DfgVertexVar' sub-type");
-            clonep->setUser<uint64_t>(component);
+            m_component[clonep] = component;
             clonep->tmpForp(vtx.tmpForp());
         }
         return clonep;
@@ -238,11 +242,11 @@ class ExtractCyclicComponents final {
 
     // Fix edges that cross components
     void fixEdges(DfgVertexVar& vtx) {
-        const uint64_t component = vtx.getUser<uint64_t>();
+        const uint64_t component = m_component.at(vtx);
 
         // Fix up srcp and dstp (they must be the same component, or variable)
         if (DfgVertex* const sp = vtx.srcp()) {
-            const uint64_t srcComponent = sp->getUser<uint64_t>();
+            const uint64_t srcComponent = m_component.at(sp);
             if (srcComponent != component) {
                 UASSERT_OBJ(sp->is<DfgVertexVar>(), &vtx, "'srcp' in different component");
                 getClone(vtx, srcComponent)->srcp(sp);
@@ -250,7 +254,7 @@ class ExtractCyclicComponents final {
             }
         }
         if (DfgVertex* const dp = vtx.defaultp()) {
-            const uint64_t defaultComponent = dp->getUser<uint64_t>();
+            const uint64_t defaultComponent = m_component.at(dp);
             if (defaultComponent != component) {
                 UASSERT_OBJ(dp->is<DfgVertexVar>(), &vtx, "'defaultp' in different component");
                 getClone(vtx, defaultComponent)->defaultp(dp);
@@ -264,7 +268,7 @@ class ExtractCyclicComponents final {
             return false;
         });
         for (DfgVertex* const sinkp : sinkps) {
-            const uint64_t sinkComponent = sinkp->getUser<uint64_t>();
+            const uint64_t sinkComponent = m_component.at(sinkp);
             // Same component is OK
             if (sinkComponent == component) continue;
             DfgVertex* const clonep = getClone(vtx, sinkComponent);
@@ -278,7 +282,7 @@ class ExtractCyclicComponents final {
     void moveVertices(DfgVertex::List<Vertex>& list) {
         for (DfgVertex* const vtxp : list.unlinkable()) {
             DfgVertex& vtx = *vtxp;
-            if (const uint64_t component = vtx.getUser<uint64_t>()) {
+            if (const uint64_t component = m_component.at(vtx)) {
                 m_dfg.removeVertex(vtx);
                 m_components[component - 1]->addVertex(vtx);
             }
@@ -289,10 +293,10 @@ class ExtractCyclicComponents final {
         // Check that edges only cross components at variable boundaries
         dfg.forEachVertex([&](DfgVertex& vtx) {
             if (vtx.is<DfgVarPacked>()) return;
-            const uint64_t component = vtx.getUser<uint64_t>();
+            const uint64_t component = m_component.at(vtx);
             vtx.foreachSink([&](DfgVertex& snk) {
                 if (snk.is<DfgVertexVar>()) return false;  // OK to cross at variables
-                UASSERT_OBJ(component == snk.getUser<uint64_t>(), &vtx,
+                UASSERT_OBJ(component == m_component.at(snk), &vtx,
                             "Edge crossing components without variable involvement");
                 return false;
             });
@@ -317,10 +321,10 @@ class ExtractCyclicComponents final {
         });
     }
 
-    void extractComponents(uint32_t numNonTrivialSCCs) {
+    void extractComponents(uint32_t nComponents) {
         // Allocate result graphs
-        m_components.resize(numNonTrivialSCCs);
-        for (uint32_t i = 0; i < numNonTrivialSCCs; ++i) {
+        m_components.resize(nComponents);
+        for (uint32_t i = 0; i < nComponents; ++i) {
             m_components[i].reset(new DfgGraph{m_dfg.modulep(), m_prefix + cvtToStr(i)});
         }
 
@@ -359,16 +363,14 @@ class ExtractCyclicComponents final {
     explicit ExtractCyclicComponents(DfgGraph& dfg, const std::string& label)
         : m_dfg{dfg}
         , m_prefix{dfg.name() + (label.empty() ? "" : "-") + label + "-component-"} {
-        // DfgVertex::user<uint64_t> is set to the SCC number by colorStronglyConnectedComponents,
-        const auto userDataInUse = dfg.userDataInUse();
         // Find all the non-trivial SCCs (and trivial cycles) in the graph
-        const uint32_t numNonTrivialSCCs = V3DfgPasses::colorStronglyConnectedComponents(dfg);
+        const uint32_t nSCCs = V3DfgPasses::colorStronglyConnectedComponents(dfg, m_component);
         // If the graph was acyclic (which should be the common case), then we are done.
-        if (!numNonTrivialSCCs) return;
+        if (!nSCCs) return;
         // Ensure that component boundaries are always at variables, by expanding SCCs
         expandComponents();
         // Extract the components
-        extractComponents(numNonTrivialSCCs);
+        extractComponents(nSCCs);
     }
 
 public:
diff --git a/src/V3DfgPasses.cpp b/src/V3DfgPasses.cpp
index 72f882ad325..f5f64e9bac8 100644
--- a/src/V3DfgPasses.cpp
+++ b/src/V3DfgPasses.cpp
@@ -30,7 +30,7 @@ void V3DfgPasses::cse(DfgGraph& dfg, V3DfgCseContext& ctx) {
     // Remove common sub-expressions
     {
         // Used by DfgVertex::hash
-        const auto userDataInUse = dfg.userDataInUse();
+        DfgUserMap<V3Hash> hashCache = dfg.makeUserMap<V3Hash>();
 
         DfgVertex::EqualsCache equalsCache;
         std::unordered_map<V3Hash, std::vector<DfgVertex*>> verticesWithEqualHashes;
@@ -38,7 +38,7 @@ void V3DfgPasses::cse(DfgGraph& dfg, V3DfgCseContext& ctx) {
 
         // Pre-hash variables, these are all unique, so just set their hash to a unique value
         uint32_t varHash = 0;
-        for (DfgVertexVar& vtx : dfg.varVertices()) vtx.user<V3Hash>() = V3Hash{++varHash};
+        for (const DfgVertexVar& vtx : dfg.varVertices()) hashCache[vtx] = V3Hash{++varHash};
 
         // Similarly pre-hash constants for speed. While we don't combine constants, we do want
         // expressions using the same constants to be combined, so we do need to hash equal
@@ -49,7 +49,7 @@ void V3DfgPasses::cse(DfgGraph& dfg, V3DfgCseContext& ctx) {
                 VL_DO_DANGLING(vtxp->unlinkDelete(dfg), vtxp);
                 continue;
             }
-            vtxp->user<V3Hash>() = vtxp->num().toHash() + varHash;
+            hashCache[vtxp] = vtxp->num().toHash() + varHash;
         }
 
         // Combine operation vertices
@@ -59,7 +59,7 @@ void V3DfgPasses::cse(DfgGraph& dfg, V3DfgCseContext& ctx) {
                 vtxp->unlinkDelete(dfg);
                 continue;
             }
-            const V3Hash hash = vtxp->hash();
+            const V3Hash hash = vtxp->hash(hashCache);
             std::vector<DfgVertex*>& vec = verticesWithEqualHashes[hash];
             bool replaced = false;
             for (DfgVertex* const candidatep : vec) {
@@ -91,8 +91,8 @@ void V3DfgPasses::inlineVars(DfgGraph& dfg) {
 }
 
 void V3DfgPasses::removeUnused(DfgGraph& dfg) {
-    // DfgVertex::user is the next pointer of the work list elements
-    const auto userDataInUse = dfg.userDataInUse();
+    // Map from vertex to next vertex in the work list
+    DfgUserMap<DfgVertex*> nextp = dfg.makeUserMap<DfgVertex*>();
 
     // Head of work list. Note that we want all next pointers in the list to be non-zero (including
     // that of the last element). This allows as to do two important things: detect if an element
@@ -102,29 +102,22 @@ void V3DfgPasses::removeUnused(DfgGraph& dfg) {
     DfgVertex* const sentinelp = reinterpret_cast<DfgVertex*>(&dfg);
     DfgVertex* workListp = sentinelp;
 
-    // Add all unused operation vertices to the work list. This also allocates all DfgVertex::user.
+    // Add all unused operation vertices to the work list
     for (DfgVertex& vtx : dfg.opVertices()) {
-        if (vtx.hasSinks()) {
-            // This vertex is used. Allocate user, but don't add to work list.
-            vtx.setUser<DfgVertex*>(nullptr);
-        } else {
-            // This vertex is unused. Add to work list.
-            vtx.setUser<DfgVertex*>(workListp);
-            workListp = &vtx;
-        }
+        if (vtx.hasSinks()) continue;
+        // This vertex is unused. Add to work list.
+        nextp[vtx] = workListp;
+        workListp = &vtx;
     }
 
     // Also add all unused temporaries created during synthesis
     for (DfgVertexVar& vtx : dfg.varVertices()) {
         if (!vtx.tmpForp()) continue;
-        if (vtx.hasSinks() || vtx.hasDfgRefs()) {
-            // This vertex is used. Allocate user, but don't add to work list.
-            vtx.setUser<DfgVertex*>(nullptr);
-        } else {
-            // This vertex is unused. Add to work list.
-            vtx.setUser<DfgVertex*>(workListp);
-            workListp = &vtx;
-        }
+        if (vtx.hasSinks()) continue;
+        if (vtx.hasDfgRefs()) continue;
+        // This vertex is unused. Add to work list.
+        nextp[vtx] = workListp;
+        workListp = &vtx;
     }
 
     // Process the work list
@@ -132,11 +125,11 @@ void V3DfgPasses::removeUnused(DfgGraph& dfg) {
         // Pick up the head
         DfgVertex* const vtxp = workListp;
         // Detach the head
-        workListp = vtxp->getUser<DfgVertex*>();
+        workListp = nextp.at(vtxp);
         // Prefetch next item
         VL_PREFETCH_RW(workListp);
         // This item is now off the work list
-        vtxp->setUser<DfgVertex*>(nullptr);
+        nextp.at(vtxp) = nullptr;
         // DfgLogic should have been synthesized or removed
         UASSERT_OBJ(!vtxp->is<DfgLogic>(), vtxp, "Should not be DfgLogic");
         // If used, then nothing to do, so move on
@@ -153,9 +146,9 @@ void V3DfgPasses::removeUnused(DfgGraph& dfg) {
             const DfgVertexVar* const varp = src.cast<DfgVertexVar>();
             if (varp && !varp->tmpForp()) return false;
             // If already in work list then nothing to do
-            if (src.getUser<DfgVertex*>()) return false;
+            if (nextp[src]) return false;
             // Actually add to work list.
-            src.setUser<DfgVertex*>(workListp);
+            nextp[src] = workListp;
             workListp = &src;
             return false;
         });
@@ -177,8 +170,6 @@ void V3DfgPasses::removeUnused(DfgGraph& dfg) {
 void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
     UASSERT(dfg.modulep(), "binToOneHot only works with unscoped DfgGraphs for now");
 
-    const auto userDataInUse = dfg.userDataInUse();
-
     // Structure to keep track of comparison details
     struct Term final {
         DfgVertex* m_vtxp = nullptr;  // Vertex to replace
@@ -189,13 +180,13 @@ void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
             , m_inv{inv} {}
     };
 
-    // Map from 'value beign compared' -> 'terms', stored in DfgVertex::user()
-    using Val2Terms = std::map<uint32_t, std::vector<Term>>;
-    // Allocator for Val2Terms, so it's cleaned up on return
-    std::deque<Val2Terms> val2TermsAllocator;
     // List of vertices that are used as sources
     std::vector<DfgVertex*> srcps;
 
+    // Map from 'vertices' -> 'value beign compared' -> 'terms'
+    using Val2Terms = std::map<uint32_t, std::vector<Term>>;
+    DfgUserMap<Val2Terms> vtx2Val2Terms = dfg.makeUserMap<Val2Terms>();
+
     // Only consider input variables from a reasonable range:
     // - not too big to avoid huge tables, you are doomed anyway at that point..
     // - not too small, as it's probably not worth it
@@ -255,16 +246,12 @@ void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
             // Not a comparison-like vertex
             continue;
         }
-        // Grab the Val2Terms entry
-        Val2Terms*& val2Termspr = srcp->user<Val2Terms*>();
-        if (!val2Termspr) {
-            // Remeber and allocate on first encounter
-            srcps.emplace_back(srcp);
-            val2TermsAllocator.emplace_back();
-            val2Termspr = &val2TermsAllocator.back();
-        }
+        // Grab Val2Terms for this vertex
+        Val2Terms& val2Terms = vtx2Val2Terms[srcp];
+        // Remeber and on first encounter
+        if (val2Terms.empty()) srcps.emplace_back(srcp);
         // Record term
-        (*val2Termspr)[val].emplace_back(&vtx, inv);
+        val2Terms[val].emplace_back(&vtx, inv);
         ++nTerms;
     }
 
@@ -281,7 +268,7 @@ void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
 
     // Create decoders for each srcp
     for (DfgVertex* const srcp : srcps) {
-        const Val2Terms& val2Terms = *srcp->getUser<Val2Terms*>();
+        const Val2Terms& val2Terms = vtx2Val2Terms[srcp];
 
         // If not enough terms in this vertex, ignore
         if (val2Terms.size() < TERM_LIMIT) continue;
@@ -422,7 +409,8 @@ void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
 }
 
 void V3DfgPasses::eliminateVars(DfgGraph& dfg, V3DfgEliminateVarsContext& ctx) {
-    const auto userDataInUse = dfg.userDataInUse();
+    // Map from vertex to next vertex in the work list
+    DfgUserMap<DfgVertex*> nextp = dfg.makeUserMap<DfgVertex*>();
 
     // Head of work list. Note that we want all next pointers in the list to be non-zero
     // (including that of the last element). This allows us to do two important things: detect
@@ -434,16 +422,16 @@ void V3DfgPasses::eliminateVars(DfgGraph& dfg, V3DfgEliminateVarsContext& ctx) {
 
     // Add all variables to the initial work list
     for (DfgVertexVar& vtx : dfg.varVertices()) {
-        vtx.setUser<DfgVertex*>(workListp);
+        nextp[vtx] = workListp;
         workListp = &vtx;
     }
 
     const auto addToWorkList = [&](DfgVertex& vtx) {
         // If already in work list then nothing to do
-        DfgVertex*& nextInWorklistp = vtx.user<DfgVertex*>();
-        if (nextInWorklistp) return false;
+        DfgVertex*& nextpr = nextp[vtx];
+        if (nextpr) return false;
         // Actually add to work list.
-        nextInWorklistp = workListp;
+        nextpr = workListp;
         workListp = &vtx;
         return false;
     };
@@ -462,9 +450,9 @@ void V3DfgPasses::eliminateVars(DfgGraph& dfg, V3DfgEliminateVarsContext& ctx) {
         // Pick up the head of the work list
         DfgVertex* const vtxp = workListp;
         // Detach the head
-        workListp = vtxp->getUser<DfgVertex*>();
+        workListp = nextp.at(vtxp);
         // Reset user pointer so it can be added back to the work list later
-        vtxp->setUser<DfgVertex*>(nullptr);
+        nextp.at(vtxp) = nullptr;
         // Prefetch next item
         VL_PREFETCH_RW(workListp);
 
diff --git a/src/V3DfgPasses.h b/src/V3DfgPasses.h
index 93febfa689c..e6c211b5ed3 100644
--- a/src/V3DfgPasses.h
+++ b/src/V3DfgPasses.h
@@ -64,14 +64,14 @@ void dfgToAst(DfgGraph&, V3DfgContext&) VL_MT_DISABLED;
 
 // Construct binary to oneHot decoders
 void binToOneHot(DfgGraph&, V3DfgBinToOneHotContext&) VL_MT_DISABLED;
-// Sets DfgVertex::user<uint64_t>() for all vertext to:
+// Populates the given DfgUserMap for all vertext to:
 // - 0, if the vertex is not part of a non-trivial strongly connected component
 //   and is not part of a self-loop. That is: the Vertex is not part of any cycle.
 // - N, if the vertex is part of a non-trivial strongly conneced component or self-loop N.
 //   That is: each set of vertices that are reachable from each other will have the same
 //   non-zero value assigned.
 // Returns the number of non-trivial SCCs (distinct cycles)
-uint32_t colorStronglyConnectedComponents(DfgGraph&) VL_MT_DISABLED;
+uint32_t colorStronglyConnectedComponents(const DfgGraph&, DfgUserMap<uint64_t>&) VL_MT_DISABLED;
 // Common subexpression elimination
 void cse(DfgGraph&, V3DfgCseContext&) VL_MT_DISABLED;
 // Inline fully driven variables
diff --git a/src/V3DfgPeephole.cpp b/src/V3DfgPeephole.cpp
index 4af6f06c8e5..4c3f0316cc8 100644
--- a/src/V3DfgPeephole.cpp
+++ b/src/V3DfgPeephole.cpp
@@ -130,6 +130,8 @@ class V3DfgPeephole final : public DfgVisitor {
 
     // STATE
     DfgGraph& m_dfg;  // The DfgGraph being visited
+    // Map from vertex to next vertex on work list
+    DfgUserMap<DfgVertex*> m_nextp = m_dfg.makeUserMap<DfgVertex*>();
     V3DfgPeepholeContext& m_ctx;  // The config structure
     AstNodeDType* const m_bitDType = V3Dfg::dtypePacked(1);  // Common, so grab it up front
     // Head of work list. Note that we want all next pointers in the list to be non-zero (including
@@ -156,9 +158,9 @@ class V3DfgPeephole final : public DfgVisitor {
         // We only process actual operation vertices
         if (vtxp->is<DfgConst>() || vtxp->is<DfgVertexVar>()) return;
         // If already in work list then nothing to do
-        if (vtxp->getUser<DfgVertex*>()) return;
+        if (m_nextp[vtxp]) return;
         // Actually add to work list.
-        vtxp->setUser<DfgVertex*>(m_workListp);
+        m_nextp[vtxp] = m_workListp;
         m_workListp = vtxp;
     }
 
@@ -181,7 +183,7 @@ class V3DfgPeephole final : public DfgVisitor {
         addSourcesToWorkList(vtxp);
         // If in work list then we can't delete it just yet (as we can't remove from the middle of
         // the work list), but it will be deleted when the work list is processed.
-        if (vtxp->getUser<DfgVertex*>()) return;
+        if (m_nextp[vtxp]) return;
         // Otherwise we can delete it now.
         // Remove from cache
         m_cache.invalidateByValue(vtxp);
@@ -230,14 +232,8 @@ class V3DfgPeephole final : public DfgVisitor {
     Vertex* make(FileLine* flp, AstNodeDType* dtypep, Operands... operands) {
         // Find or create an equivalent vertex
         Vertex* const vtxp = m_cache.getOrCreate<Vertex, Operands...>(flp, dtypep, operands...);
-
-        // Add to work list.
-        DfgVertex*& workListNextp = vtxp->template user<DfgVertex*>();
-        if (!workListNextp) {
-            workListNextp = m_workListp;
-            m_workListp = vtxp;
-        }
-
+        // Add to work list
+        addToWorkList(vtxp);
         // Return new node
         return vtxp;
     }
@@ -1755,13 +1751,9 @@ class V3DfgPeephole final : public DfgVisitor {
         : m_dfg{dfg}
         , m_ctx{ctx} {
 
-        // DfgVertex::user is the next pointer of the work list elements
-        const auto userDataInUse = m_dfg.userDataInUse();
-
-        // Add all vertices to the work list, and to the vertex cache.
-        // This also allocates all DfgVertex::user.
+        // Add all operation vertices to the work list and cache
         for (DfgVertex& vtx : m_dfg.opVertices()) {
-            vtx.setUser<DfgVertex*>(m_workListp);
+            m_nextp[vtx] = m_workListp;
             m_workListp = &vtx;
             m_cache.cache(&vtx);
         }
@@ -1771,9 +1763,9 @@ class V3DfgPeephole final : public DfgVisitor {
             // Pick up the head
             DfgVertex* const vtxp = m_workListp;
             // Detach the head and prefetch next
-            m_workListp = vtxp->getUser<DfgVertex*>();
+            m_workListp = m_nextp[vtxp];
             VL_PREFETCH_RW(m_workListp);
-            vtxp->setUser<DfgVertex*>(nullptr);
+            m_nextp[vtxp] = nullptr;
             // Remove unused vertices as we gp
             if (!vtxp->hasSinks()) {
                 deleteVertex(vtxp);
diff --git a/src/V3DfgSynthesize.cpp b/src/V3DfgSynthesize.cpp
index 16d3badadd9..9840f1583ee 100644
--- a/src/V3DfgSynthesize.cpp
+++ b/src/V3DfgSynthesize.cpp
@@ -1822,8 +1822,8 @@ void V3DfgPasses::synthesize(DfgGraph& dfg, V3DfgContext& ctx) {
         // Otherwise figure out which vertices are worth synthesizing.
 
         // Find cycles
-        const auto userDataInUse = dfg.userDataInUse();
-        V3DfgPasses::colorStronglyConnectedComponents(dfg);
+        DfgUserMap<uint64_t> scc = dfg.makeUserMap<uint64_t>();
+        V3DfgPasses::colorStronglyConnectedComponents(dfg, scc);
 
         // First, gather variables, we will then attempt to synthesize all their drivers
         std::vector<DfgVertexVar*> varps;
@@ -1832,7 +1832,7 @@ void V3DfgPasses::synthesize(DfgGraph& dfg, V3DfgContext& ctx) {
             if (!var.srcp()) continue;
 
             // Circular variable - synthesize
-            if (var.getUser<uint64_t>()) {
+            if (scc.at(var)) {
                 varps.emplace_back(&var);
                 continue;
             }
