From f2d45bbf0ed167f015f55d23e6eeaaaba765f301 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 3 Jul 2026 22:51:42 +0200 Subject: [PATCH] nix search: Reduce evaluator worker starvation Off-CPU profiling showed that ~2 of 12 worker threads were idle on average during the parallel phase of 'nix search', because work items were only spawned after fully enumerating each attrset: while one thread enumerated legacyPackages.x86_64-linux (~120k attributes), and later each large package subset (pythonPackages, perlPackages, ...), the other workers had nothing new to pick up. Two changes: * Spawn work items incrementally (every 256 attributes) during enumeration instead of in one batch at the end, so idle workers can start on the first attributes while enumeration continues. * Executor::spawn(): Generate queue keys with a thread-local mt19937_64 instead of calling std::random_device per work item, which costs hundreds of cycles per call (RDRAND / /dev/urandom). The key only needs to spread same-priority items around the queue, not be cryptographically random. At 12 eval cores (with GC disabled via GC_INITIAL_HEAP_SIZE=40G), this reduces elapsed time from ~4.6s to ~4.3s, user CPU from ~28s to ~26s, and kernel time from ~5s to ~3.9s for 'nix search nixpkgs --no-eval-cache fizzbuzz'. Assisted-by: Claude Fable 5 --- src/libexpr/parallel-eval.cc | 9 +++++++-- src/nix/search.cc | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libexpr/parallel-eval.cc b/src/libexpr/parallel-eval.cc index a2955961e063..e0cdab93a7b8 100644 --- a/src/libexpr/parallel-eval.cc +++ b/src/libexpr/parallel-eval.cc @@ -134,10 +134,15 @@ std::vector> Executor::spawn(WorkItems && items) for (auto & item : items) { std::promise promise; futures.push_back(promise.get_future()); - [[gnu::tls_model("initial-exec")]] static thread_local std::random_device rd; + /* Note: this uses a cheap PRNG rather than std::random_device, + since the latter costs hundreds of cycles per call (RDRAND or + /dev/urandom), which adds up when spawning many work items. The + key only needs to spread items of the same priority around the + queue, not be cryptographically random. */ + [[gnu::tls_model("initial-exec")]] static thread_local std::mt19937_64 rng{std::random_device{}()}; [[gnu::tls_model("initial-exec")]] static thread_local std::uniform_int_distribution dist( 0, 1ULL << 48); - auto key = (uint64_t(item.second) << 48) | dist(rd); + auto key = (uint64_t(item.second) << 48) | dist(rng); state->queue.emplace(key, Item{.promise = std::move(promise), .work = std::move(item.first)}); } } diff --git a/src/nix/search.cc b/src/nix/search.cc index d1cebbac67a9..4d1abf24a690 100644 --- a/src/nix/search.cc +++ b/src/nix/search.cc @@ -114,6 +114,12 @@ struct CmdSearch : InstallableValueCommand, MixJSON work, std::string_view(state->symbols[attr]).find("Packages") != std::string_view::npos ? 0 : 2, [cursor2, attrPath2, visit]() { visit(*cursor2, attrPath2, false); }); + /* Spawn incrementally rather than after the whole + enumeration, so that idle worker threads can start + on the first attributes while we're still + enumerating the rest. */ + if (work.size() >= 256) + futures.spawn(std::exchange(work, {})); } futures.spawn(std::move(work)); };