Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 36 additions & 23 deletions packaging/dependencies.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,43 @@ scope: {
inherit stdenv;
}).overrideAttrs
(attrs: {
# Increase the initial mark stack size to avoid stack
# overflows, since these inhibit parallel marking (see
# GC_mark_some()). To check whether the mark stack is too
# small, run Nix with GC_PRINT_STATS=1 and look for messages
# such as `Mark stack overflow`, `No room to copy back mark
# stack`, and `Grew mark stack to ... frames`.
NIX_CFLAGS_COMPILE = [
"-DINITIAL_MARK_STACK_SIZE=1048576"
]
# For some reason that is not clear, it is wanting to use libgcc_eh which is not available.
# Force this to be built with compiler-rt & libunwind over libgcc_eh works.
# Issue: https://github.com/NixOS/nixpkgs/issues/177129
++
lib.optionals
(
stdenv.cc.isClang
&& stdenv.hostPlatform.isStatic
&& stdenv.cc.libcxx != null
&& stdenv.cc.libcxx.isLLVM
)
# Reduce contention on the GC allocation lock during parallel
# evaluation by handing out multiple heap blocks worth of
# objects per lock acquisition in GC_generic_malloc_many().
# The default batch size is set via GC_MANY_BLOCKS_DEFAULT
# below and can be overridden at runtime through the
# GC_MALLOC_MANY_BLOCKS environment variable.
patches = (attrs.patches or [ ]) ++ [ ./patches/boehmgc-batch-malloc-many.patch ];

env = (attrs.env or { }) // {
# Increase the initial mark stack size to avoid stack
# overflows, since these inhibit parallel marking (see
# GC_mark_some()). To check whether the mark stack is too
# small, run Nix with GC_PRINT_STATS=1 and look for messages
# such as `Mark stack overflow`, `No room to copy back mark
# stack`, and `Grew mark stack to ... frames`.
NIX_CFLAGS_COMPILE = toString (
[
"-rtlib=compiler-rt"
"-unwindlib=libunwind"
];
"-DINITIAL_MARK_STACK_SIZE=1048576"
"-DGC_MANY_BLOCKS_DEFAULT=64"
]
# For some reason that is not clear, it is wanting to use libgcc_eh which is not available.
# Force this to be built with compiler-rt & libunwind over libgcc_eh works.
# Issue: https://github.com/NixOS/nixpkgs/issues/177129
++
lib.optionals
(
stdenv.cc.isClang
&& stdenv.hostPlatform.isStatic
&& stdenv.cc.libcxx != null
&& stdenv.cc.libcxx.isLLVM
)
[
"-rtlib=compiler-rt"
"-unwindlib=libunwind"
]
);
};

buildInputs =
(attrs.buildInputs or [ ])
Expand Down
113 changes: 113 additions & 0 deletions packaging/patches/boehmgc-batch-malloc-many.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
Reduce contention on the GC allocation lock: make GC_generic_malloc_many()
hand out up to GC_many_blocks heap blocks worth of objects per acquisition
of the allocation lock, instead of exactly one. This matters for
parallel evaluation in Nix, where all evaluator threads replenish their
thread-local free lists (Values, Envs, Bindings) through this function
and otherwise serialize on the allocation lock.

The batch size is settable via the GC_MALLOC_MANY_BLOCKS environment
variable (1..64); the compile-time default is GC_MANY_BLOCKS_DEFAULT
(1 = upstream behavior, overridden in packaging/dependencies.nix).

diff -ur a/mallocx.c b/mallocx.c
--- a/mallocx.c 2026-07-03 12:56:51.389341768 +0200
+++ b/mallocx.c 2026-07-03 13:01:54.873023343 +0200
@@ -300,6 +300,16 @@
/* since the collector would not retain the entire list if it were */
/* invoked just as we were returning. */
/* Note that the client should usually clear the link field. */
+/* Number of heap blocks worth of objects to hand out per allocation */
+/* lock acquisition. Larger values reduce contention on the */
+/* allocation lock when many threads allocate small objects heavily. */
+/* Settable via the GC_MALLOC_MANY_BLOCKS environment variable. */
+#ifndef GC_MANY_BLOCKS_DEFAULT
+# define GC_MANY_BLOCKS_DEFAULT 1
+#endif
+#define GC_MANY_BLOCKS_MAX 64
+int GC_many_blocks = GC_MANY_BLOCKS_DEFAULT;
+
GC_API void GC_CALL GC_generic_malloc_many(size_t lb, int k, void **result)
{
void *op;
@@ -433,7 +443,7 @@
my_bytes_allocd = 0;
for (p = op; p != 0; p = obj_link(p)) {
my_bytes_allocd += lb;
- if ((word)my_bytes_allocd >= HBLKSIZE) {
+ if ((word)my_bytes_allocd >= (word)GC_many_blocks * HBLKSIZE) {
*opp = obj_link(p);
obj_link(p) = 0;
break;
@@ -442,12 +452,24 @@
GC_bytes_allocd += my_bytes_allocd;
goto out;
}
- /* Next try to allocate a new block worth of objects of this size. */
+ /* Next try to allocate new blocks worth of objects of this size. */
+ /* Up to GC_many_blocks heap blocks are allocated per acquisition */
+ /* of the allocation lock, to reduce contention on it. */
{
- struct hblk *h = GC_allochblk(lb, k, 0);
- if (h /* != NULL */) { /* CPPCHECK */
- if (IS_UNCOLLECTABLE(k)) GC_set_hdr_marks(HDR(h));
+ struct hblk *hbs[GC_MANY_BLOCKS_MAX];
+ int nblocks = GC_many_blocks;
+ int i;
+
+ if (nblocks < 1) nblocks = 1;
+ if (nblocks > GC_MANY_BLOCKS_MAX) nblocks = GC_MANY_BLOCKS_MAX;
+ for (i = 0; i < nblocks; i++) {
+ hbs[i] = GC_allochblk(lb, k, 0);
+ if (NULL == hbs[i]) break;
+ if (IS_UNCOLLECTABLE(k)) GC_set_hdr_marks(HDR(hbs[i]));
GC_bytes_allocd += HBLKSIZE - HBLKSIZE % lb;
+ }
+ nblocks = i;
+ if (nblocks > 0) {
# ifdef PARALLEL_MARK
if (GC_parallel) {
GC_acquire_mark_lock();
@@ -455,8 +477,10 @@
UNLOCK();
GC_release_mark_lock();

- op = GC_build_fl(h, lw,
- (ok -> ok_init || GC_debugging_started), 0);
+ op = 0;
+ for (i = 0; i < nblocks; i++)
+ op = GC_build_fl(hbs[i], lw,
+ (ok -> ok_init || GC_debugging_started), (ptr_t)op);

*result = op;
GC_acquire_mark_lock();
@@ -467,7 +491,10 @@
return;
}
# endif
- op = GC_build_fl(h, lw, (ok -> ok_init || GC_debugging_started), 0);
+ op = 0;
+ for (i = 0; i < nblocks; i++)
+ op = GC_build_fl(hbs[i], lw,
+ (ok -> ok_init || GC_debugging_started), (ptr_t)op);
goto out;
}
}
diff -ur a/misc.c b/misc.c
--- a/misc.c 2026-07-03 12:56:51.390341791 +0200
+++ b/misc.c 2026-07-03 13:01:54.874023365 +0200
@@ -1157,6 +1157,15 @@
}
}
{
+ extern int GC_many_blocks;
+ char * many_blocks_string = GETENV("GC_MALLOC_MANY_BLOCKS");
+ if (many_blocks_string != NULL) {
+ int many_blocks = atoi(many_blocks_string);
+ if (many_blocks > 0 && many_blocks <= 64)
+ GC_many_blocks = many_blocks;
+ }
+ }
+ {
char * space_divisor_string = GETENV("GC_FREE_SPACE_DIVISOR");
if (space_divisor_string != NULL) {
int space_divisor = atoi(space_divisor_string);
3 changes: 2 additions & 1 deletion src/libcmd/include/nix/cmd/installable-attr-path.hh
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@

#include <regex>
#include <queue>
#include "nix/expr/root-value.hh"

namespace nix {

class InstallableAttrPath : public InstallableValue
{
SourceExprCommand & cmd;
RootValue v;
UniqueRootValue v;
std::string attrPath;
ExtendedOutputsSpec extendedOutputsSpec;

Expand Down
2 changes: 1 addition & 1 deletion src/libcmd/installable-attr-path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ InstallableAttrPath::InstallableAttrPath(
ExtendedOutputsSpec extendedOutputsSpec)
: InstallableValue(state)
, cmd(cmd)
, v(allocRootValue(v))
, v(UniqueRootValue(v))
, attrPath(attrPath)
, extendedOutputsSpec(std::move(extendedOutputsSpec))
{
Expand Down
4 changes: 2 additions & 2 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ void NixRepl::loadDebugTraceEnv(DebugTrace & dt)

// add staticenv vars.
for (auto & [name, value] : *(vm.get()))
addVarToScope(state->symbols.create(name), *value);
addVarToScope(state->symbols.create(name), **value);
}
}

Expand Down Expand Up @@ -932,7 +932,7 @@ ReplExitStatus AbstractNixRepl::runSimple(ref<EvalState> evalState, const ValMap

// add 'extra' vars.
for (auto & [name, value] : extraEnv)
repl->addVarToScope(repl->state->symbols.create(name), *value);
repl->addVarToScope(repl->state->symbols.create(name), **value);

return repl->mainLoop();
}
Expand Down
8 changes: 4 additions & 4 deletions src/libexpr/eval-cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ Value * EvalCache::getRootValue()
{
if (!value) {
debug("getting root value");
value = allocRootValue(rootLoader());
value = UniqueRootValue(rootLoader());
}
return *value;
}
Expand All @@ -378,7 +378,7 @@ AttrCursor::AttrCursor(
, cachedValue(std::move(cachedValue))
{
if (value)
_value = allocRootValue(value);
_value = UniqueRootValue(value);
}

AttrKey AttrCursor::getKey()
Expand All @@ -401,9 +401,9 @@ Value & AttrCursor::getValue()
auto attr = vParent.attrs()->get(parent->second);
if (!attr)
throw Error("attribute '%s' is unexpectedly missing", getAttrPathStr());
_value = allocRootValue(attr->value);
_value = UniqueRootValue(attr->value);
} else
_value = allocRootValue(root->getRootValue());
_value = UniqueRootValue(root->getRootValue());
}
return **_value;
}
Expand Down
50 changes: 29 additions & 21 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,6 @@ const StringData & StringData::make(EvalMemory & mem, std::string_view s)
return res;
}

RootValue allocRootValue(Value * v)
{
return std::allocate_shared<Value *>(traceable_allocator<Value *>(), v);
}

// Pretty print types for assertion errors
std::ostream & operator<<(std::ostream & os, const ValueType t)
{
Expand Down Expand Up @@ -580,7 +575,7 @@ Value * EvalState::addPrimOp(PrimOp && primOp)
v->mkPrimOp(new PrimOp(primOp));

if (primOp.internal)
internalPrimOps.emplace(primOp.name, v);
internalPrimOps.emplace(primOp.name, UniqueRootValue(v));
else {
staticBaseEnv->vars.emplace_back(envName, baseEnvDispl);
baseEnv.values[baseEnvDispl++] = v;
Expand Down Expand Up @@ -754,11 +749,11 @@ void mapStaticEnvBindings(const SymbolTable & st, const StaticEnv & se, const En
if (se.isWith && env.values[0]->isFinished()) {
// add 'with' bindings.
for (auto & j : *env.values[0]->attrs())
vm.insert_or_assign(std::string(st[j.name]), j.value);
vm.insert_or_assign(std::string(st[j.name]), UniqueRootValue(j.value));
} else {
// iterate through staticenv bindings and add them.
for (auto & i : se.vars)
vm.insert_or_assign(std::string(st[i.first]), env.values[i.second]);
vm.insert_or_assign(std::string(st[i.first]), UniqueRootValue(env.values[i.second]));
}
}
}
Expand Down Expand Up @@ -1167,24 +1162,28 @@ void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
importResolutionCache->emplace(path, *resolvedPath);
}

if (auto v2 = getConcurrent(*fileEvalCache, *resolvedPath)) {
forceValue(**v2, noPos);
v = **v2;
return;
{
Value * v2 = nullptr;
fileEvalCache->cvisit(*resolvedPath, [&](auto & i) { v2 = *i.second; });
if (v2) {
forceValue(*v2, noPos);
v = *v2;
return;
}
}

Value * vExpr;
ExprParseFile expr{*resolvedPath, mustBeTrivial};

fileEvalCache->try_emplace_and_cvisit(
*resolvedPath,
nullptr,
UniqueRootValue(nullptr),
[&](auto & i) {
vExpr = allocValue();
vExpr->mkThunk(&baseEnv, &expr);
i.second = vExpr;
*i.second = vExpr;
},
[&](auto & i) { vExpr = i.second; });
[&](auto & i) { vExpr = *i.second; });

forceValue(*vExpr, noPos);

Expand Down Expand Up @@ -1366,7 +1365,12 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v)
sort = true;
}

bindings.bindings->pos = pos;
/* Empty attrsets share the static Bindings::emptyBindings, which we
must not write to: apart from being a data race, it causes false
sharing on emptyBindings' cache line (which may also hold other hot
globals such as Counter::enabled) between all evaluator threads. */
if (bindings.bindings != &Bindings::emptyBindings)
bindings.bindings->pos = pos;

v.mkAttrs(sort ? bindings.finish() : bindings.alreadySorted());
}
Expand Down Expand Up @@ -3137,14 +3141,16 @@ SourcePath resolveExprPath(SourcePath path, bool addDefaultNix)
// Basic cycle/depth limit to avoid infinite loops.
if (++followCount >= maxFollow)
throw Error("too many symbolic links encountered while traversing the path '%s'", path);
auto p = path.parent().resolveSymlinks() / path.baseName();
if (p.lstat().type != SourceAccessor::tSymlink)
auto parent = path.path.parent().value_or(CanonPath::root);
auto p = path.accessor->resolveSymlinks(parent) / *path.path.baseName();
if (path.accessor->lstat(p).type != SourceAccessor::tSymlink)
break;
path = {path.accessor, CanonPath(p.readLink(), path.path.parent().value_or(CanonPath::root))};
path.path = CanonPath(path.accessor->readLink(p), parent);
}

/* If `path' refers to a directory, append `/default.nix'. */
if (addDefaultNix && path.resolveSymlinks().lstat().type == SourceAccessor::tDirectory)
if (addDefaultNix
&& path.accessor->lstat(path.accessor->resolveSymlinks(path.path)).type == SourceAccessor::tDirectory)
return path / "default.nix";

return path;
Expand All @@ -3157,7 +3163,9 @@ Expr * EvalState::parseExprFromFile(const SourcePath & path)

Expr * EvalState::parseExprFromFile(const SourcePath & path, const std::shared_ptr<StaticEnv> & staticEnv)
{
auto buffer = path.resolveSymlinks().readFile();
/* Avoid a SourcePath temporary (accessor ref + path string copy);
this runs once per parsed file. */
auto buffer = path.accessor->readFile(path.accessor->resolveSymlinks(path.path));
// readFile hopefully have left some extra space for terminators
buffer.append("\0\0", 2);
return parse(buffer.data(), buffer.size(), Pos::Origin(path), path.parent(), staticEnv);
Expand Down
5 changes: 3 additions & 2 deletions src/libexpr/include/nix/expr/eval-cache.hh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <functional>
#include <variant>
#include "nix/expr/root-value.hh"

namespace nix::eval_cache {

Expand Down Expand Up @@ -44,7 +45,7 @@ public:
private:
typedef fun<Value *()> RootLoader;
RootLoader rootLoader;
RootValue value;
UniqueRootValue value;

Value * getRootValue();

Expand Down Expand Up @@ -111,7 +112,7 @@ public:
private:
using Parent = std::optional<std::pair<ref<AttrCursor>, Symbol>>;
const Parent parent;
RootValue _value;
UniqueRootValue _value;
std::optional<std::pair<AttrId, AttrValue>> cachedValue;

AttrKey getKey();
Expand Down
Loading