reduce the size of Env by one pointer

since `up` and `values` are both pointer-aligned the type field will
also be pointer-aligned, wasting 48 bits of space on most machines. we
can get away with removing the type field altogether by encoding some
information into the `with` expr that created the env to begin with,
reducing the GC load for the absolutely massive amount of single-entry
envs we create for lambdas. this reduces memory usage of system eval by
quite a bit (reducing heap size of our system eval from 8.4GB to 8.23GB)
and gives similar savings in eval time.

running `nix eval --raw --impure --expr 'with import <nixpkgs/nixos> {}; system'`

before:

  Time (mean ± σ):      5.576 s ±  0.003 s    [User: 5.197 s, System: 0.378 s]
  Range (min … max):    5.572 s …  5.581 s    10 runs

after:

  Time (mean ± σ):      5.408 s ±  0.002 s    [User: 5.019 s, System: 0.388 s]
  Range (min … max):    5.405 s …  5.411 s    10 runs
This commit is contained in:
pennae 2023-12-22 18:19:53 +01:00
parent ee439734e9
commit 1fe66852ff
9 changed files with 75 additions and 34 deletions

View file

@ -0,0 +1,7 @@
---
synopsis: Reduce eval memory usage and wall time
prs: 9658
---
Reduce the size of the `Env` struct used in the evaluator by a pointer, or 8 bytes on most modern machines.
This reduces memory usage during eval by around 2% and wall time by around 3%.

View file

@ -0,0 +1,31 @@
---
synopsis: Better error reporting for `with` expressions
prs: 9658
---
`with` expressions using non-attrset values to resolve variables are now reported with proper positions.
Previously an incorrect `with` expression would report no position at all, making it hard to determine where the error originated:
```
nix-repl> with 1; a
error:
<borked>
at «none»:0: (source not available)
error: value is an integer while a set was expected
```
Now position information is preserved and reported as with most other errors:
```
nix-repl> with 1; a
error:
… while evaluating the first subexpression of a with expression
at «string»:1:1:
1| with 1; a
| ^
error: value is an integer while a set was expected
```

View file

@ -112,7 +112,7 @@ NixRepl::NixRepl(const SearchPath & searchPath, nix::ref<Store> store, ref<EvalS
: AbstractNixRepl(state) : AbstractNixRepl(state)
, debugTraceIndex(0) , debugTraceIndex(0)
, getValues(getValues) , getValues(getValues)
, staticEnv(new StaticEnv(false, state->staticBaseEnv.get())) , staticEnv(new StaticEnv(nullptr, state->staticBaseEnv.get()))
, historyFile(getDataDir() + "/nix/repl-history") , historyFile(getDataDir() + "/nix/repl-history")
{ {
} }

View file

@ -73,8 +73,6 @@ Env & EvalState::allocEnv(size_t size)
#endif #endif
env = (Env *) allocBytes(sizeof(Env) + size * sizeof(Value *)); env = (Env *) allocBytes(sizeof(Env) + size * sizeof(Value *));
env->type = Env::Plain;
/* We assume that env->values has been cleared by the allocator; maybeThunk() and lookupVar fromWith expect this. */ /* We assume that env->values has been cleared by the allocator; maybeThunk() and lookupVar fromWith expect this. */
return *env; return *env;

View file

@ -543,7 +543,7 @@ EvalState::EvalState(
, env1AllocCache(std::allocate_shared<void *>(traceable_allocator<void *>(), nullptr)) , env1AllocCache(std::allocate_shared<void *>(traceable_allocator<void *>(), nullptr))
#endif #endif
, baseEnv(allocEnv(128)) , baseEnv(allocEnv(128))
, staticBaseEnv{std::make_shared<StaticEnv>(false, nullptr)} , staticBaseEnv{std::make_shared<StaticEnv>(nullptr, nullptr)}
{ {
corepkgsFS->setPathDisplay("<nix", ">"); corepkgsFS->setPathDisplay("<nix", ">");
internalFS->setPathDisplay("«nix-internal»", ""); internalFS->setPathDisplay("«nix-internal»", "");
@ -781,7 +781,7 @@ void printStaticEnvBindings(const SymbolTable & st, const StaticEnv & se)
// just for the current level of Env, not the whole chain. // just for the current level of Env, not the whole chain.
void printWithBindings(const SymbolTable & st, const Env & env) void printWithBindings(const SymbolTable & st, const Env & env)
{ {
if (env.type == Env::HasWithAttrs) { if (!env.values[0]->isThunk()) {
std::cout << "with: "; std::cout << "with: ";
std::cout << ANSI_MAGENTA; std::cout << ANSI_MAGENTA;
Bindings::iterator j = env.values[0]->attrs->begin(); Bindings::iterator j = env.values[0]->attrs->begin();
@ -835,7 +835,7 @@ void mapStaticEnvBindings(const SymbolTable & st, const StaticEnv & se, const En
if (env.up && se.up) { if (env.up && se.up) {
mapStaticEnvBindings(st, *se.up, *env.up, vm); mapStaticEnvBindings(st, *se.up, *env.up, vm);
if (env.type == Env::HasWithAttrs) { if (!env.values[0]->isThunk()) {
// add 'with' bindings. // add 'with' bindings.
Bindings::iterator j = env.values[0]->attrs->begin(); Bindings::iterator j = env.values[0]->attrs->begin();
while (j != env.values[0]->attrs->end()) { while (j != env.values[0]->attrs->end()) {
@ -973,22 +973,23 @@ inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval)
if (!var.fromWith) return env->values[var.displ]; if (!var.fromWith) return env->values[var.displ];
// This early exit defeats the `maybeThunk` optimization for variables from `with`,
// The added complexity of handling this appears to be similarly in cost, or
// the cases where applicable were insignificant in the first place.
if (noEval) return nullptr;
auto * fromWith = var.fromWith;
while (1) { while (1) {
if (env->type == Env::HasWithExpr) { forceAttrs(*env->values[0], fromWith->pos, "while evaluating the first subexpression of a with expression");
if (noEval) return 0;
Value * v = allocValue();
evalAttrs(*env->up, (Expr *) env->values[0], *v, noPos, "<borked>");
env->values[0] = v;
env->type = Env::HasWithAttrs;
}
Bindings::iterator j = env->values[0]->attrs->find(var.name); Bindings::iterator j = env->values[0]->attrs->find(var.name);
if (j != env->values[0]->attrs->end()) { if (j != env->values[0]->attrs->end()) {
if (countCalls) attrSelects[j->pos]++; if (countCalls) attrSelects[j->pos]++;
return j->value; return j->value;
} }
if (!env->prevWith) if (!fromWith->parentWith)
error("undefined variable '%1%'", symbols[var.name]).atPos(var.pos).withFrame(*env, var).debugThrow<UndefinedVarError>(); error("undefined variable '%1%'", symbols[var.name]).atPos(var.pos).withFrame(*env, var).debugThrow<UndefinedVarError>();
for (size_t l = env->prevWith; l; --l, env = env->up) ; for (size_t l = fromWith->prevWith; l; --l, env = env->up) ;
fromWith = fromWith->parentWith;
} }
} }
@ -1816,9 +1817,7 @@ void ExprWith::eval(EvalState & state, Env & env, Value & v)
{ {
Env & env2(state.allocEnv(1)); Env & env2(state.allocEnv(1));
env2.up = &env; env2.up = &env;
env2.prevWith = prevWith; env2.values[0] = attrs->maybeThunk(state, env);
env2.type = Env::HasWithExpr;
env2.values[0] = (Value *) attrs;
body->eval(state, env2, v); body->eval(state, env2, v);
} }

View file

@ -116,11 +116,6 @@ struct Constant
struct Env struct Env
{ {
Env * up; Env * up;
/**
* Number of of levels up to next `with` environment
*/
unsigned short prevWith:14;
enum { Plain = 0, HasWithExpr, HasWithAttrs } type:2;
Value * values[0]; Value * values[0];
}; };

View file

@ -333,6 +333,8 @@ void ExprVar::bindVars(EvalState & es, const std::shared_ptr<const StaticEnv> &
if (es.debugRepl) if (es.debugRepl)
es.exprEnvs.insert(std::make_pair(this, env)); es.exprEnvs.insert(std::make_pair(this, env));
fromWith = nullptr;
/* Check whether the variable appears in the environment. If so, /* Check whether the variable appears in the environment. If so,
set its level and displacement. */ set its level and displacement. */
const StaticEnv * curEnv; const StaticEnv * curEnv;
@ -344,7 +346,6 @@ void ExprVar::bindVars(EvalState & es, const std::shared_ptr<const StaticEnv> &
} else { } else {
auto i = curEnv->find(name); auto i = curEnv->find(name);
if (i != curEnv->vars.end()) { if (i != curEnv->vars.end()) {
fromWith = false;
this->level = level; this->level = level;
displ = i->second; displ = i->second;
return; return;
@ -360,7 +361,8 @@ void ExprVar::bindVars(EvalState & es, const std::shared_ptr<const StaticEnv> &
.msg = hintfmt("undefined variable '%1%'", es.symbols[name]), .msg = hintfmt("undefined variable '%1%'", es.symbols[name]),
.errPos = es.positions[pos] .errPos = es.positions[pos]
}); });
fromWith = true; for (auto * e = env.get(); e && !fromWith; e = e->up)
fromWith = e->isWith;
this->level = withLevel; this->level = withLevel;
} }
@ -393,7 +395,7 @@ void ExprAttrs::bindVars(EvalState & es, const std::shared_ptr<const StaticEnv>
es.exprEnvs.insert(std::make_pair(this, env)); es.exprEnvs.insert(std::make_pair(this, env));
if (recursive) { if (recursive) {
auto newEnv = std::make_shared<StaticEnv>(false, env.get(), recursive ? attrs.size() : 0); auto newEnv = std::make_shared<StaticEnv>(nullptr, env.get(), recursive ? attrs.size() : 0);
Displacement displ = 0; Displacement displ = 0;
for (auto & i : attrs) for (auto & i : attrs)
@ -435,7 +437,7 @@ void ExprLambda::bindVars(EvalState & es, const std::shared_ptr<const StaticEnv>
es.exprEnvs.insert(std::make_pair(this, env)); es.exprEnvs.insert(std::make_pair(this, env));
auto newEnv = std::make_shared<StaticEnv>( auto newEnv = std::make_shared<StaticEnv>(
false, env.get(), nullptr, env.get(),
(hasFormals() ? formals->formals.size() : 0) + (hasFormals() ? formals->formals.size() : 0) +
(!arg ? 0 : 1)); (!arg ? 0 : 1));
@ -471,7 +473,7 @@ void ExprLet::bindVars(EvalState & es, const std::shared_ptr<const StaticEnv> &
if (es.debugRepl) if (es.debugRepl)
es.exprEnvs.insert(std::make_pair(this, env)); es.exprEnvs.insert(std::make_pair(this, env));
auto newEnv = std::make_shared<StaticEnv>(false, env.get(), attrs->attrs.size()); auto newEnv = std::make_shared<StaticEnv>(nullptr, env.get(), attrs->attrs.size());
Displacement displ = 0; Displacement displ = 0;
for (auto & i : attrs->attrs) for (auto & i : attrs->attrs)
@ -490,6 +492,10 @@ void ExprWith::bindVars(EvalState & es, const std::shared_ptr<const StaticEnv> &
if (es.debugRepl) if (es.debugRepl)
es.exprEnvs.insert(std::make_pair(this, env)); es.exprEnvs.insert(std::make_pair(this, env));
parentWith = nullptr;
for (auto * e = env.get(); e && !parentWith; e = e->up)
parentWith = e->isWith;
/* Does this `with' have an enclosing `with'? If so, record its /* Does this `with' have an enclosing `with'? If so, record its
level so that `lookupVar' can look up variables in the previous level so that `lookupVar' can look up variables in the previous
`with' if this one doesn't contain the desired attribute. */ `with' if this one doesn't contain the desired attribute. */
@ -506,7 +512,7 @@ void ExprWith::bindVars(EvalState & es, const std::shared_ptr<const StaticEnv> &
es.exprEnvs.insert(std::make_pair(this, env)); es.exprEnvs.insert(std::make_pair(this, env));
attrs->bindVars(es, env); attrs->bindVars(es, env);
auto newEnv = std::make_shared<StaticEnv>(true, env.get()); auto newEnv = std::make_shared<StaticEnv>(this, env.get());
body->bindVars(es, newEnv); body->bindVars(es, newEnv);
} }

View file

@ -138,6 +138,7 @@ std::ostream & operator << (std::ostream & str, const Pos & pos);
struct Env; struct Env;
struct Value; struct Value;
class EvalState; class EvalState;
struct ExprWith;
struct StaticEnv; struct StaticEnv;
@ -226,8 +227,11 @@ struct ExprVar : Expr
Symbol name; Symbol name;
/* Whether the variable comes from an environment (e.g. a rec, let /* Whether the variable comes from an environment (e.g. a rec, let
or function argument) or from a "with". */ or function argument) or from a "with".
bool fromWith;
`nullptr`: Not from a `with`.
Valid pointer: the nearest, innermost `with` expression to query first. */
ExprWith * fromWith;
/* In the former case, the value is obtained by going `level` /* In the former case, the value is obtained by going `level`
levels up from the current environment and getting the levels up from the current environment and getting the
@ -385,6 +389,7 @@ struct ExprWith : Expr
PosIdx pos; PosIdx pos;
Expr * attrs, * body; Expr * attrs, * body;
size_t prevWith; size_t prevWith;
ExprWith * parentWith;
ExprWith(const PosIdx & pos, Expr * attrs, Expr * body) : pos(pos), attrs(attrs), body(body) { }; ExprWith(const PosIdx & pos, Expr * attrs, Expr * body) : pos(pos), attrs(attrs), body(body) { };
PosIdx getPos() const override { return pos; } PosIdx getPos() const override { return pos; }
COMMON_METHODS COMMON_METHODS
@ -478,14 +483,14 @@ extern ExprBlackHole eBlackHole;
runtime. */ runtime. */
struct StaticEnv struct StaticEnv
{ {
bool isWith; ExprWith * isWith;
const StaticEnv * up; const StaticEnv * up;
// Note: these must be in sorted order. // Note: these must be in sorted order.
typedef std::vector<std::pair<Symbol, Displacement>> Vars; typedef std::vector<std::pair<Symbol, Displacement>> Vars;
Vars vars; Vars vars;
StaticEnv(bool isWith, const StaticEnv * up, size_t expectedSize = 0) : isWith(isWith), up(up) { StaticEnv(ExprWith * isWith, const StaticEnv * up, size_t expectedSize = 0) : isWith(isWith), up(up) {
vars.reserve(expectedSize); vars.reserve(expectedSize);
}; };

View file

@ -214,7 +214,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v
Env * env = &state.allocEnv(vScope->attrs->size()); Env * env = &state.allocEnv(vScope->attrs->size());
env->up = &state.baseEnv; env->up = &state.baseEnv;
auto staticEnv = std::make_shared<StaticEnv>(false, state.staticBaseEnv.get(), vScope->attrs->size()); auto staticEnv = std::make_shared<StaticEnv>(nullptr, state.staticBaseEnv.get(), vScope->attrs->size());
unsigned int displ = 0; unsigned int displ = 0;
for (auto & attr : *vScope->attrs) { for (auto & attr : *vScope->attrs) {