Skip to content

Allocate RootValues more efficiently#546

Merged
cole-h merged 4 commits into
mainfrom
root-values
Jul 8, 2026
Merged

Allocate RootValues more efficiently#546
cole-h merged 4 commits into
mainfrom
root-values

Conversation

@edolstra

@edolstra edolstra commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Taken from #534.

RootValues were expensive for parallel eval, since each RootValue caused a Boehm GC call to allocate traceable-but-uncollectable memory while holding the global GC root. We now allocate slab of uncollectable memory from which the RootValues are allocated.

Also, there is a new UniqueRootValue type which is like RootValue except that it isn't wrapped in an std::shared_ptr, so it entirely avoids a heap allocation. Most instances of traceable_allocator<Value *> are now a UniqueRootValue.

Speedup:

eval-cores-compare

Context

Summary by CodeRabbit

  • Bug Fixes

    • Improved robustness of expression evaluation, especially around caching behavior, REPL variable injection, and JSON parsing, reducing the likelihood of crashes or inconsistent results in edge cases.
    • Enhanced stability when handling attribute paths and internal primitive operations.
  • Chores

    • Added and wired up new runtime support for safer value lifetime management, including build and installed-header updates.

edolstra added 3 commits July 8, 2026 14:53
Previously every RootValue was created with
std::allocate_shared<Value *>(traceable_allocator<Value *>()), which
costs a GC_MALLOC_UNCOLLECTABLE() / GC_FREE() pair per root value.
Uncollectable allocations always take the global GC allocation lock,
making this a significant source of mutex contention during parallel
evaluation: the eval cache allocates a root value per AttrCursor
(i.e. per attribute visited by 'nix search'), and the parallel
evaluator allocates one per queued work item.

Instead, carve root slots out of large uncollectable slabs (which are
permanently part of the GC root set) and recycle them through a free
list protected by a plain mutex, whose critical section is a few
instructions rather than the entire GC allocator. Slots are cleared
on release so they don't keep values alive; liveness semantics are
unchanged. The slabs are never freed, so pool memory is bounded by
the peak number of simultaneously live root values (8 bytes per
slot).

At 16 eval cores (with GC disabled via GC_INITIAL_HEAP_SIZE=40G),
this cuts kernel time from ~8-9s to ~7s, context switches from 1.46M
to 0.99M, and elapsed time from ~4.9s to ~4.3-4.5s for 'nix search
nixpkgs --no-eval-cache fizzbuzz'.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
RootValue (std::shared_ptr<Value *>) adds avoidable overhead per GC
root: a malloc'd control block, atomic reference count updates on
copies (a measured source of cache line contention during parallel
evaluation), and 16 bytes per handle. Most users never copy their
roots, so introduce UniqueRootValue, a move-only RAII wrapper around
a pointer to a root slot that returns the slot to the pool on
destruction.

The pool internals move from allocRootValue() into
allocRootValueSlot() / freeRootValueSlot(), shared by both handle
types, and the whole root value machinery moves into new files
root-value.{hh,cc}. The non-Boehm fallback now also goes through the
slot functions (new/delete) instead of a make_shared special case.

Converted to UniqueRootValue: ValMap, fileEvalCache, internalPrimOps,
genericClosure's work/result lists, EvalCache::value,
AttrCursor::_value, InstallableAttrPath::v, and the JSON parser
state. RootValue remains (documented as such) for roots captured in
std::function-backed lambdas, which require copyability.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
Replace the Sync<std::vector<Value **>> free slot pool with an
intrusive linked list: each slot is a union of Value * (in use) and a
pointer to the next free slot, with a Sync<Slot *> head. Allocation
pops the head; freeing pushes in O(1).

This avoids the auxiliary vector memory (8 bytes per free slot), the
4096-iteration push_back loop when carving a new slab, and most
importantly the possibility of a vector reallocation (a malloc) while
holding the pool lock on the free path.

GC safety is unchanged: slabs remain uncollectable and conservatively
scanned. In-use slots contain a Value * and root the value; free
slots contain a pointer into a slab (or null), which is scanned
harmlessly since slabs are never freed anyway. Writing the next
pointer on free overwrites the Value *, which doubles as the "stop
rooting the value" step.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 193cd3aa-4836-4c31-a432-74bd6fac451a

📥 Commits

Reviewing files that changed from the base of the PR and between bb1f958 and 7cf421a.

📒 Files selected for processing (1)
  • src/libexpr/include/nix/expr/root-value.hh
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/libexpr/include/nix/expr/root-value.hh

📝 Walkthrough

Walkthrough

Replaces the shared_ptr-based RootValue rooting API with a slot-pooled UniqueRootValue handle and updates eval, cache, JSON, primop, REPL, and installable-attr-path call sites to use it.

Changes

UniqueRootValue migration

Layer / File(s) Summary
New root-value slot pool and UniqueRootValue type
src/libexpr/include/nix/expr/root-value.hh, src/libexpr/root-value.cc, src/libexpr/include/nix/expr/value.hh, src/libexpr/include/nix/expr/meson.build, src/libexpr/meson.build
Adds allocRootValueSlot/freeRootValueSlot, UniqueRootValue, and RootValue/allocRootValue; implements the slot pool; removes the old declarations from value.hh; adds the new files to the build.
Core eval.hh/eval.cc migration
src/libexpr/include/nix/expr/eval.hh, src/libexpr/eval.cc
ValMap, fileEvalCache, and internalPrimOps now store UniqueRootValue; allocRootValue is removed; addPrimOp, mapStaticEnvBindings, and evalFile use the new wrappers.
EvalCache and AttrCursor migration
src/libexpr/include/nix/expr/eval-cache.hh, src/libexpr/eval-cache.cc
EvalCache::value and AttrCursor::_value switch to UniqueRootValue; constructors and lookup paths are updated.
Downstream consumers
src/libcmd/include/nix/cmd/installable-attr-path.hh, src/libcmd/installable-attr-path.cc, src/libcmd/repl.cc, src/libexpr/json-to-value.cc, src/libexpr/primops.cc, src/libexpr/primops/fetchTree.cc, src/libexpr/include/nix/expr/get-drvs.hh
Updates installable path storage, REPL scope injection, JSON SAX rooting, prim_genericClosure, internal primop dereference depth, and a FIXME comment to align with UniqueRootValue.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: cole-h

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: making RootValue allocation more efficient.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch root-values

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

@github-actions github-actions Bot temporarily deployed to pull request July 8, 2026 14:55 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/libexpr/eval.cc`:
- Around line 1165-1171: The cache-hit fast path in eval.cc bypasses the
direct-attrset validation and can return a value cached under a looser
mustBeTrivial setting, so update the file-eval cache handling in the
resolvedPath lookup path to preserve the mustBeTrivial contract. Either
incorporate mustBeTrivial into the fileEvalCache key used by the cvisit/lookup
logic, or re-run the same triviality check before accepting a cached Value in
the eval path that assigns v from v2.

In `@src/libexpr/include/nix/expr/get-drvs.hh`:
- Around line 35-36: PackageInfo is storing raw Bindings pointers for
attrs/meta, but the backing Value needs to be rooted instead to keep the attrset
alive. Update the PackageInfo storage and related accessors to hold a rooted
Value or equivalent bindings-aware handle, and derive attrs/meta from that
rooted value rather than trying to apply UniqueRootValue directly to the pointer
fields. Use PackageInfo and its attrs/meta members as the main symbols when
making the change.

In `@src/libexpr/include/nix/expr/root-value.hh`:
- Around line 50-55: Guard UniqueRootValue::operator=(UniqueRootValue&&) against
self-move assignment by checking whether this and other are the same object
before freeing the current slot. If they are identical, return immediately;
otherwise release the existing slot, then transfer other.slot with std::exchange
as before so x = std::move(x) cannot return the same slot to the pool and leave
the object holding a freed slot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80257c66-152f-4579-9f5e-d265eae8f244

📥 Commits

Reviewing files that changed from the base of the PR and between b54da83 and bb1f958.

📒 Files selected for processing (16)
  • src/libcmd/include/nix/cmd/installable-attr-path.hh
  • src/libcmd/installable-attr-path.cc
  • src/libcmd/repl.cc
  • src/libexpr/eval-cache.cc
  • src/libexpr/eval.cc
  • src/libexpr/include/nix/expr/eval-cache.hh
  • src/libexpr/include/nix/expr/eval.hh
  • src/libexpr/include/nix/expr/get-drvs.hh
  • src/libexpr/include/nix/expr/meson.build
  • src/libexpr/include/nix/expr/root-value.hh
  • src/libexpr/include/nix/expr/value.hh
  • src/libexpr/json-to-value.cc
  • src/libexpr/meson.build
  • src/libexpr/primops.cc
  • src/libexpr/primops/fetchTree.cc
  • src/libexpr/root-value.cc

Comment thread src/libexpr/eval.cc
Comment thread src/libexpr/include/nix/expr/get-drvs.hh
Comment thread src/libexpr/include/nix/expr/root-value.hh
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@github-actions github-actions Bot temporarily deployed to pull request July 8, 2026 15:12 Inactive
@cole-h cole-h added this pull request to the merge queue Jul 8, 2026
Merged via the queue into main with commit 2f47be4 Jul 8, 2026
33 checks passed
@cole-h cole-h deleted the root-values branch July 8, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants