perf: Move entries off NodePathStack instead of copying them

Five sites copied the top entry out and then popped it. `SHAMapTreeNodePtr` is
refcounted, so each copy bumped its atomic strong-ref count; `release()` moves
the pointer out instead, a plain pointer swap with no atomic. `SHAMapNodeID`
also derives from `CountedObject`, but `CountedObject` declares no move
constructor, so moving a `SHAMapNodeID` still runs its copy constructor;
`release()` saves nothing on the ID half of the pair. `dirtyUp` and `delItem`
walk up to 64 levels per insert or delete on the ledger write path, so this
removes up to 64 atomic operations per call, not 128. The two sites that read
without popping now bind a reference rather than copying.

`dirtyUp` and `delItem` both drop a `dynamicPointerCast`/null check for a
static cast, on the reasoning that by the time either receives the stack,
`addGiveItem`/`updateGiveItem` have already consumed the terminal leaf entry
via `release()`, so every remaining entry is provably an inner node. That
reasoning holds today, but an `XRPL_ASSERT` is a no-op under `NDEBUG`, so
both get a real `UNREACHABLE`-guarded check instead: a release build that
somehow violated the invariant would otherwise write through a
misinterpreted node via `setChild`, silent memory corruption in place of the
clean crash `dynamicPointerCast` used to produce. `updateGiveItem`'s own
cast needed the same treatment for a different reason: an absent tag leaves
an inner node on top of the stack, and the cast that followed the assertion
there would have reinterpreted an `SHAMapInnerNode` as a `SHAMapLeafNode`.
Replaced with `if (!top->isLeaf()) return false;`, pinned by a regression
test.

The `std::move` these sites previously applied to
`staticPointerCast`/`dynamicPointerCast` was dropped rather than fixed: both
only had a `TT const&` overload, so the move bound to that const ref and
copied anyway, silently defeating the `SHAMapTreeNodePtr` refcount saving
described above. Adds the missing rvalue overload to each, tied to
`SharedIntrusive<TT>&&` rather than a bare `TT&&` so it cannot also bind to
an lvalue in preference to the const-ref overload, and restores `std::move`
at the three call sites that own a soon-to-be-discarded pointer.
This commit is contained in:
Bart
2026-08-02 06:40:54 -04:00
parent ee6ddfbfdb
commit e41e469e02
4 changed files with 92 additions and 17 deletions

View File

@@ -529,11 +529,30 @@ staticPointerCast(TT const& v)
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, v);
}
// A bare `TT&&` here would be a forwarding reference, since TT is deduced directly from this
// parameter, and would then also bind to lvalues in preference to the `const&` overload above
// (binding to a plain reference beats binding to a const one), silently moving out of a caller's
// live variable on what looks like a copy call. Naming the wrapped type keeps TT nested inside
// SharedIntrusive<TT>, so this only binds to an actual rvalue of that type.
template <class T, class TT>
SharedPtr<T>
staticPointerCast(SharedIntrusive<TT>&& v)
{
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, std::move(v));
}
template <class T, class TT>
SharedPtr<T>
dynamicPointerCast(TT const& v)
{
return SharedPtr<T>(DynamicCastTagSharedIntrusive{}, v);
}
template <class T, class TT>
SharedPtr<T>
dynamicPointerCast(SharedIntrusive<TT>&& v)
{
return SharedPtr<T>(DynamicCastTagSharedIntrusive{}, std::move(v));
}
} // namespace intr_ptr
} // namespace xrpl

View File

@@ -490,6 +490,28 @@ private:
stack_ = {};
}
/**
* Remove and return the node at the end of the path.
*
* Copying an entry out before popping costs an atomic increment on the node's refcount,
* and callers that pop immediately do this per level, so on a 64-level path it is
* measurable. Moving avoids that increment.
*/
[[nodiscard]] std::pair<SHAMapTreeNodePtr, SHAMapNodeID>
release()
{
if (stack_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::release : empty stack");
return {};
// LCOV_EXCL_STOP
}
auto entry = std::move(stack_.top());
stack_.pop();
return entry;
}
/**
* Start a path at the root of the map, whose ID is the zero-depth ID by definition.
*
@@ -601,6 +623,10 @@ private:
* Walk towards the specified id, returning the node. Caller must check
* if the return is nullptr, and if not, if the node->peekItem()->key() ==
* id
*
* @param stack records the path walked, or nullptr to skip recording it. Lookups that only
* want the leaf (see findKey) omit it to avoid building a path they would
* immediately discard.
*/
SHAMapLeafNode*
walkTowardsKey(uint256 const& id, NodePathStack* stack = nullptr) const;

View File

@@ -111,10 +111,15 @@ SHAMap::dirtyUp(NodePathStack& stack, uint256 const& target, SHAMapTreeNodePtr c
while (!stack.empty())
{
auto node = intr_ptr::dynamicPointerCast<SHAMapInnerNode>(stack.top().first);
SHAMapNodeID const nodeID = stack.top().second;
stack.pop();
XRPL_ASSERT(node, "xrpl::SHAMap::dirtyUp : non-null node");
auto [top, nodeID] = stack.release();
if (!top->isInner())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::dirtyUp : node is not inner");
Throw<SHAMapMissingNode>(type_, target);
// LCOV_EXCL_STOP
}
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(std::move(top));
auto const branch = selectBranch(nodeID, target);
@@ -553,7 +558,7 @@ SHAMap::peekNextItem(uint256 const& id, NodePathStack& stack) const
stack.pop();
while (!stack.empty())
{
auto const [node, nodeID] = stack.top();
auto const& [node, nodeID] = stack.top();
XRPL_ASSERT(!node->isLeaf(), "xrpl::SHAMap::peekNextItem : another node is not leaf");
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
for (auto i = selectBranch(nodeID, id) + 1; i < kBranchFactor; ++i)
@@ -611,7 +616,7 @@ SHAMap::boundHelper(uint256 const& id, BelowDirection direction) const
walkTowardsKey(id, &stack);
while (!stack.empty())
{
auto const [node, nodeID] = stack.top();
auto const& [node, nodeID] = stack.top();
if (node->isLeaf())
{
auto const& item = safeDowncast<SHAMapLeafNode const&>(*node).peekItem();
@@ -675,8 +680,7 @@ SHAMap::delItem(uint256 const& id)
if (stack.empty())
Throw<SHAMapMissingNode>(type_, id);
auto leaf = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.top().first);
stack.pop();
auto leaf = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.release().first);
if (!leaf || (leaf->peekItem()->key() != id))
return false;
@@ -688,9 +692,15 @@ SHAMap::delItem(uint256 const& id)
while (!stack.empty())
{
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(stack.top().first);
SHAMapNodeID const nodeID = stack.top().second;
stack.pop();
auto [top, nodeID] = stack.release();
if (!top->isInner())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::delItem : node is not inner");
Throw<SHAMapMissingNode>(type_, id);
// LCOV_EXCL_STOP
}
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(std::move(top));
node = unshareNode(std::move(node), nodeID);
node->setChild(
@@ -761,8 +771,7 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
if (stack.empty())
Throw<SHAMapMissingNode>(type_, tag);
auto [node, nodeID] = stack.top();
stack.pop();
auto [node, nodeID] = stack.release();
if (node->isLeaf())
{
@@ -849,11 +858,17 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
if (stack.empty())
Throw<SHAMapMissingNode>(type_, tag);
auto node = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.top().first);
auto nodeID = stack.top().second;
stack.pop();
auto [top, nodeID] = stack.release();
if (!node || (node->peekItem()->key() != tag))
// walkTowardsKey pushes an inner node's own entry before testing whether the branch it needs
// is empty, so a tag absent from the map leaves that inner node on top rather than a leaf.
// Not reachable through updateGiveItem's current callers, all of which check the item exists
// before calling this, but the static cast below is safe only once this is confirmed.
if (!top->isLeaf())
return false;
auto node = intr_ptr::staticPointerCast<SHAMapLeafNode>(std::move(top));
if (node->peekItem()->key() != tag)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::updateGiveItem : invalid node");

View File

@@ -417,6 +417,21 @@ TEST_F(SHAMapTraversal, bounds_agree_with_iteration_for_absent_keys)
}
}
TEST_F(SHAMapTraversal, update_give_item_on_absent_key_returns_false)
{
tests::TestNodeFamily f{j_};
auto keys = deepFanOutKeys();
SHAMap map{SHAMapType::FREE, f};
fillMap(map, keys);
// Absent key: walkTowardsKey stops on an inner node with an empty branch, not a leaf.
auto const absentKey = uint256{std::string_view{std::string(64, '0')}};
Buffer vuc{32};
std::fill_n(vuc.data(), vuc.size(), std::uint8_t{2});
EXPECT_FALSE(map.updateGiveItem(
SHAMapNodeType::TnAccountState, makeShamapitem(absentKey, std::move(vuc))));
}
TEST_F(SHAMapTraversal, bounds_on_empty_map_return_end)
{
tests::TestNodeFamily f{j_};