Optimize primop calls

We now parse function applications as a vector of arguments rather
than as a chain of binary applications, e.g. 'substring 1 2 "foo"' is
parsed as

  ExprCall { .fun = <substring>, .args = [ <1>, <2>, <"foo"> ] }

rather than

  ExprApp (ExprApp (ExprApp <substring> <1>) <2>) <"foo">

This allows primops to be called immediately (if enough arguments are
supplied) without having to allocate intermediate tPrimOpApp values.

On

  $ nix-instantiate --dry-run '<nixpkgs/nixos/release-combined.nix>' -A nixos.tests.simple.x86_64-linux

this gives a substantial performance improvement:

  user CPU time:      median =      0.9209  mean =      0.9218  stddev =      0.0073  min =      0.9086  max =      0.9340  [rejected, p=0.00000, Δ=-0.21433±0.00677]
  elapsed time:       median =      1.0585  mean =      1.0584  stddev =      0.0024  min =      1.0523  max =      1.0623  [rejected, p=0.00000, Δ=-0.20594±0.00236]

because it reduces the number of tPrimOpApp allocations from 551990 to
42534 (i.e. only small minority of primop calls are partially
applied) which in turn reduces time spent in the garbage collector.
This commit is contained in:
Eelco Dolstra 2020-02-24 01:32:01 +01:00
parent ab35cbd675
commit 81e7c40264
6 changed files with 228 additions and 138 deletions

View file

@ -594,6 +594,8 @@ Value * EvalState::addConstant(const string & name, Value & v)
Value * EvalState::addPrimOp(const string & name, Value * EvalState::addPrimOp(const string & name,
size_t arity, PrimOpFun primOp) size_t arity, PrimOpFun primOp)
{ {
assert(arity <= maxPrimOpArity);
auto name2 = string(name, 0, 2) == "__" ? string(name, 2) : name; auto name2 = string(name, 0, 2) == "__" ? string(name, 2) : name;
Symbol sym = symbols.create(name2); Symbol sym = symbols.create(name2);
@ -1251,144 +1253,182 @@ void ExprLambda::eval(EvalState & state, Env & env, Value & v)
} }
void ExprApp::eval(EvalState & state, Env & env, Value & v) void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & vRes, const Pos & pos)
{
/* FIXME: vFun prevents GCC from doing tail call optimisation. */
Value vFun;
e1->eval(state, env, vFun);
state.callFunction(vFun, *(e2->maybeThunk(state, env)), v, pos);
}
void EvalState::callPrimOp(Value & fun, Value & arg, Value & v, const Pos & pos)
{
/* Figure out the number of arguments still needed. */
size_t argsDone = 0;
Value * primOp = &fun;
while (primOp->isPrimOpApp()) {
argsDone++;
primOp = primOp->primOpApp.left;
}
assert(primOp->isPrimOp());
auto arity = primOp->primOp->arity;
auto argsLeft = arity - argsDone;
if (argsLeft == 1) {
/* We have all the arguments, so call the primop. */
/* Put all the arguments in an array. */
Value * vArgs[arity];
auto n = arity - 1;
vArgs[n--] = &arg;
for (Value * arg = &fun; arg->isPrimOpApp(); arg = arg->primOpApp.left)
vArgs[n--] = arg->primOpApp.right;
/* And call the primop. */
nrPrimOpCalls++;
if (countCalls) primOpCalls[primOp->primOp->name]++;
primOp->primOp->fun(*this, pos, vArgs, v);
} else {
Value * fun2 = allocValue();
*fun2 = fun;
v.mkPrimOpApp(fun2, &arg);
}
}
void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & pos)
{ {
auto trace = evalSettings.traceFunctionCalls ? std::make_unique<FunctionCallTrace>(pos) : nullptr; auto trace = evalSettings.traceFunctionCalls ? std::make_unique<FunctionCallTrace>(pos) : nullptr;
forceValue(fun, pos); forceValue(fun, pos);
if (fun.isPrimOp() || fun.isPrimOpApp()) { Value vCur(fun);
callPrimOp(fun, arg, v, pos);
return;
}
if (fun.type() == nAttrs) { auto makeAppChain = [&]()
auto found = fun.attrs->find(sFunctor); {
if (found != fun.attrs->end()) { vRes = vCur;
/* fun may be allocated on the stack of the calling function, for (size_t i = 0; i < nrArgs; ++i) {
* but for functors we may keep a reference, so heap-allocate auto fun2 = allocValue();
* a copy and use that instead. *fun2 = vRes;
*/ vRes.mkPrimOpApp(fun2, args[i]);
auto & fun2 = *allocValue(); }
fun2 = fun; };
/* !!! Should we use the attr pos here? */
Value v2;
callFunction(*found->value, fun2, v2, pos);
return callFunction(v2, arg, v, pos);
}
}
if (!fun.isLambda()) while (nrArgs > 0) {
throwTypeError(pos, "attempt to call something which is not a function but %1%", fun);
ExprLambda & lambda(*fun.lambda.fun); if (vCur.isLambda()) {
auto size = ExprLambda & lambda(*vCur.lambda.fun);
(lambda.arg.empty() ? 0 : 1) +
(lambda.hasFormals() ? lambda.formals->formals.size() : 0);
Env & env2(allocEnv(size));
env2.up = fun.lambda.env;
size_t displ = 0; auto size =
(lambda.arg.empty() ? 0 : 1) +
(lambda.hasFormals() ? lambda.formals->formals.size() : 0);
Env & env2(allocEnv(size));
env2.up = vCur.lambda.env;
if (!lambda.hasFormals()) size_t displ = 0;
env2.values[displ++] = &arg;
else { if (!lambda.hasFormals())
forceAttrs(arg, pos); env2.values[displ++] = args[0];
if (!lambda.arg.empty()) else {
env2.values[displ++] = &arg; forceAttrs(*args[0], pos);
/* For each formal argument, get the actual argument. If if (!lambda.arg.empty())
there is no matching actual argument but the formal env2.values[displ++] = args[0];
argument has a default, use the default. */
size_t attrsUsed = 0; /* For each formal argument, get the actual argument. If
for (auto & i : lambda.formals->formals) { there is no matching actual argument but the formal
Bindings::iterator j = arg.attrs->find(i.name); argument has a default, use the default. */
if (j == arg.attrs->end()) { size_t attrsUsed = 0;
if (!i.def) throwTypeError(pos, "%1% called without required argument '%2%'", for (auto & i : lambda.formals->formals) {
lambda, i.name); auto j = args[0]->attrs->get(i.name);
env2.values[displ++] = i.def->maybeThunk(*this, env2); if (!j) {
if (!i.def) throwTypeError(pos, "%1% called without required argument '%2%'",
lambda, i.name);
env2.values[displ++] = i.def->maybeThunk(*this, env2);
} else {
attrsUsed++;
env2.values[displ++] = j->value;
}
}
/* Check that each actual argument is listed as a formal
argument (unless the attribute match specifies a `...'). */
if (!lambda.formals->ellipsis && attrsUsed != args[0]->attrs->size()) {
/* Nope, so show the first unexpected argument to the
user. */
for (auto & i : *args[0]->attrs)
if (lambda.formals->argNames.find(i.name) == lambda.formals->argNames.end())
throwTypeError(pos, "%1% called with unexpected argument '%2%'", lambda, i.name);
abort(); // can't happen
}
}
nrFunctionCalls++;
if (countCalls) incrFunctionCall(&lambda);
/* Evaluate the body. */
try {
lambda.body->eval(*this, env2, vCur);
} catch (Error & e) {
if (loggerSettings.showTrace.get()) {
addErrorTrace(e, lambda.pos, "while evaluating %s",
(lambda.name.set()
? "'" + (string) lambda.name + "'"
: "anonymous lambda"));
addErrorTrace(e, pos, "from call site%s", "");
}
throw;
}
nrArgs--;
args += 1;
}
else if (vCur.isPrimOp()) {
size_t argsLeft = vCur.primOp->arity;
if (nrArgs < argsLeft) {
/* We don't have enough arguments, so create a tPrimOpApp chain. */
makeAppChain();
return;
} else { } else {
attrsUsed++; /* We have all the arguments, so call the primop. */
env2.values[displ++] = j->value; nrPrimOpCalls++;
if (countCalls) primOpCalls[vCur.primOp->name]++;
vCur.primOp->fun(*this, pos, args, vCur);
nrArgs -= argsLeft;
args += argsLeft;
} }
} }
/* Check that each actual argument is listed as a formal else if (vCur.isPrimOpApp()) {
argument (unless the attribute match specifies a `...'). */ /* Figure out the number of arguments still needed. */
if (!lambda.formals->ellipsis && attrsUsed != arg.attrs->size()) { size_t argsDone = 0;
/* Nope, so show the first unexpected argument to the Value * primOp = &vCur;
user. */ while (primOp->isPrimOpApp()) {
for (auto & i : *arg.attrs) argsDone++;
if (lambda.formals->argNames.find(i.name) == lambda.formals->argNames.end()) primOp = primOp->primOpApp.left;
throwTypeError(pos, "%1% called with unexpected argument '%2%'", lambda, i.name); }
abort(); // can't happen assert(primOp->isPrimOp());
auto arity = primOp->primOp->arity;
auto argsLeft = arity - argsDone;
if (nrArgs < argsLeft) {
/* We still don't have enough arguments, so extend the tPrimOpApp chain. */
makeAppChain();
return;
} else {
/* We have all the arguments, so call the primop with
the previous and new arguments. */
Value * vArgs[arity];
auto n = argsDone;
for (Value * arg = &vCur; arg->isPrimOpApp(); arg = arg->primOpApp.left)
vArgs[--n] = arg->primOpApp.right;
for (size_t i = 0; i < argsLeft; ++i)
vArgs[argsDone + i] = args[i];
nrPrimOpCalls++;
if (countCalls) primOpCalls[primOp->primOp->name]++;
primOp->primOp->fun(*this, pos, vArgs, vCur);
nrArgs -= argsLeft;
args += argsLeft;
}
} }
else if (vCur.type() == nAttrs) {
if (auto functor = vCur.attrs->get(sFunctor)) {
/* 'vCur" may be allocated on the stack of the calling
function, but for functors we may keep a reference,
so heap-allocate a copy and use that instead. */
Value * args2[] = {allocValue()};
*args2[0] = vCur;
/* !!! Should we use the attr pos here? */
callFunction(*functor->value, 1, args2, vCur, pos);
}
}
else
throwTypeError(pos, "attempt to call something which is not a function but %1%", vCur);
} }
nrFunctionCalls++; vRes = vCur;
if (countCalls) incrFunctionCall(&lambda); }
/* Evaluate the body. This is conditional on showTrace, because
catching exceptions makes this function not tail-recursive. */ void ExprCall::eval(EvalState & state, Env & env, Value & v)
if (loggerSettings.showTrace.get()) {
try { Value vFun;
lambda.body->eval(*this, env2, v); fun->eval(state, env, vFun);
} catch (Error & e) {
addErrorTrace(e, lambda.pos, "while evaluating %s", Value * vArgs[args.size()];
(lambda.name.set() for (size_t i = 0; i < args.size(); ++i)
? "'" + (string) lambda.name + "'" vArgs[i] = args[i]->maybeThunk(state, env);
: "anonymous lambda"));
addErrorTrace(e, pos, "from call site%s", ""); state.callFunction(vFun, args.size(), vArgs, v, pos);
throw;
}
else
fun.lambda.fun->body->eval(*this, env2, v);
} }

View file

@ -277,6 +277,8 @@ private:
Value * addConstant(const string & name, Value & v); Value * addConstant(const string & name, Value & v);
constexpr static size_t maxPrimOpArity = 3;
Value * addPrimOp(const string & name, Value * addPrimOp(const string & name,
size_t arity, PrimOpFun primOp); size_t arity, PrimOpFun primOp);
@ -316,8 +318,14 @@ public:
bool isFunctor(Value & fun); bool isFunctor(Value & fun);
void callFunction(Value & fun, Value & arg, Value & v, const Pos & pos); // FIXME: use std::span
void callPrimOp(Value & fun, Value & arg, Value & v, const Pos & pos); void callFunction(Value & fun, size_t nrArgs, Value * * args, Value & vRes, const Pos & pos);
void callFunction(Value & fun, Value & arg, Value & vRes, const Pos & pos)
{
Value * args[] = {&arg};
callFunction(fun, 1, args, vRes, pos);
}
/* Automatically call a function for which each argument has a /* Automatically call a function for which each argument has a
default value or has a binding in the `args' map. */ default value or has a binding in the `args' map. */

View file

@ -64,6 +64,7 @@ static void adjustLoc(YYLTYPE * loc, const char * s, size_t len)
} }
// FIXME: optimize
static Expr * unescapeStr(SymbolTable & symbols, const char * s, size_t length) static Expr * unescapeStr(SymbolTable & symbols, const char * s, size_t length)
{ {
string t; string t;

View file

@ -143,6 +143,16 @@ void ExprLambda::show(std::ostream & str) const
str << ": " << *body << ")"; str << ": " << *body << ")";
} }
void ExprCall::show(std::ostream & str) const
{
str << '(' << *fun;
for (auto e : args) {
str << ' ';
str << *e;
}
str << ')';
}
void ExprLet::show(std::ostream & str) const void ExprLet::show(std::ostream & str) const
{ {
str << "(let "; str << "(let ";
@ -366,6 +376,13 @@ void ExprLambda::bindVars(const StaticEnv & env)
body->bindVars(newEnv); body->bindVars(newEnv);
} }
void ExprCall::bindVars(const StaticEnv & env)
{
fun->bindVars(env);
for (auto e : args)
e->bindVars(env);
}
void ExprLet::bindVars(const StaticEnv & env) void ExprLet::bindVars(const StaticEnv & env)
{ {
StaticEnv newEnv(false, &env, attrs->attrs.size()); StaticEnv newEnv(false, &env, attrs->attrs.size());
@ -461,5 +478,4 @@ size_t SymbolTable::totalSize() const
return n; return n;
} }
} }

View file

@ -248,6 +248,17 @@ struct ExprLambda : Expr
COMMON_METHODS COMMON_METHODS
}; };
struct ExprCall : Expr
{
Expr * fun;
std::vector<Expr *> args;
Pos pos;
ExprCall(const Pos & pos, Expr * fun, std::vector<Expr *> && args)
: fun(fun), args(args), pos(pos)
{ }
COMMON_METHODS
};
struct ExprLet : Expr struct ExprLet : Expr
{ {
ExprAttrs * attrs; ExprAttrs * attrs;
@ -306,7 +317,6 @@ struct ExprOpNot : Expr
void eval(EvalState & state, Env & env, Value & v); \ void eval(EvalState & state, Env & env, Value & v); \
}; };
MakeBinOp(ExprApp, "")
MakeBinOp(ExprOpEq, "==") MakeBinOp(ExprOpEq, "==")
MakeBinOp(ExprOpNEq, "!=") MakeBinOp(ExprOpNEq, "!=")
MakeBinOp(ExprOpAnd, "&&") MakeBinOp(ExprOpAnd, "&&")

View file

@ -41,6 +41,12 @@ namespace nix {
{ }; { };
}; };
// Helper to prevent an expensive dynamic_cast call in expr_app.
struct App
{
Expr * e;
bool isCall;
};
} }
#define YY_DECL int yylex \ #define YY_DECL int yylex \
@ -280,10 +286,12 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * err
char * uri; char * uri;
std::vector<nix::AttrName> * attrNames; std::vector<nix::AttrName> * attrNames;
std::vector<nix::Expr *> * string_parts; std::vector<nix::Expr *> * string_parts;
nix::App app; // bool == whether this is an ExprCall
} }
%type <e> start expr expr_function expr_if expr_op %type <e> start expr expr_function expr_if expr_op
%type <e> expr_app expr_select expr_simple %type <e> expr_select expr_simple
%type <app> expr_app
%type <list> expr_list %type <list> expr_list
%type <attrs> binds %type <attrs> binds
%type <formals> formals %type <formals> formals
@ -353,13 +361,13 @@ expr_if
expr_op expr_op
: '!' expr_op %prec NOT { $$ = new ExprOpNot($2); } : '!' expr_op %prec NOT { $$ = new ExprOpNot($2); }
| '-' expr_op %prec NEGATE { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__sub")), new ExprInt(0)), $2); } | '-' expr_op %prec NEGATE { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__sub")), {new ExprInt(0), $2}); }
| expr_op EQ expr_op { $$ = new ExprOpEq($1, $3); } | expr_op EQ expr_op { $$ = new ExprOpEq($1, $3); }
| expr_op NEQ expr_op { $$ = new ExprOpNEq($1, $3); } | expr_op NEQ expr_op { $$ = new ExprOpNEq($1, $3); }
| expr_op '<' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__lessThan")), $1), $3); } | expr_op '<' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$1, $3}); }
| expr_op LEQ expr_op { $$ = new ExprOpNot(new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__lessThan")), $3), $1)); } | expr_op LEQ expr_op { $$ = new ExprOpNot(new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$3, $1})); }
| expr_op '>' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__lessThan")), $3), $1); } | expr_op '>' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$3, $1}); }
| expr_op GEQ expr_op { $$ = new ExprOpNot(new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__lessThan")), $1), $3)); } | expr_op GEQ expr_op { $$ = new ExprOpNot(new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$1, $3})); }
| expr_op AND expr_op { $$ = new ExprOpAnd(CUR_POS, $1, $3); } | expr_op AND expr_op { $$ = new ExprOpAnd(CUR_POS, $1, $3); }
| expr_op OR expr_op { $$ = new ExprOpOr(CUR_POS, $1, $3); } | expr_op OR expr_op { $$ = new ExprOpOr(CUR_POS, $1, $3); }
| expr_op IMPL expr_op { $$ = new ExprOpImpl(CUR_POS, $1, $3); } | expr_op IMPL expr_op { $$ = new ExprOpImpl(CUR_POS, $1, $3); }
@ -367,17 +375,24 @@ expr_op
| expr_op '?' attrpath { $$ = new ExprOpHasAttr($1, *$3); } | expr_op '?' attrpath { $$ = new ExprOpHasAttr($1, *$3); }
| expr_op '+' expr_op | expr_op '+' expr_op
{ $$ = new ExprConcatStrings(CUR_POS, false, new vector<Expr *>({$1, $3})); } { $$ = new ExprConcatStrings(CUR_POS, false, new vector<Expr *>({$1, $3})); }
| expr_op '-' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__sub")), $1), $3); } | expr_op '-' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__sub")), {$1, $3}); }
| expr_op '*' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__mul")), $1), $3); } | expr_op '*' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__mul")), {$1, $3}); }
| expr_op '/' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__div")), $1), $3); } | expr_op '/' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__div")), {$1, $3}); }
| expr_op CONCAT expr_op { $$ = new ExprOpConcatLists(CUR_POS, $1, $3); } | expr_op CONCAT expr_op { $$ = new ExprOpConcatLists(CUR_POS, $1, $3); }
| expr_app | expr_app { $$ = $1.e; }
; ;
expr_app expr_app
: expr_app expr_select : expr_app expr_select {
{ $$ = new ExprApp(CUR_POS, $1, $2); } if ($1.isCall) {
| expr_select { $$ = $1; } ((ExprCall *) $1.e)->args.push_back($2);
$$ = $1;
} else {
$$.e = new ExprCall(CUR_POS, $1.e, {$2});
$$.isCall = true;
}
}
| expr_select { $$.e = $1; $$.isCall = false; }
; ;
expr_select expr_select
@ -388,7 +403,7 @@ expr_select
| /* Backwards compatibility: because Nixpkgs has a rarely used | /* Backwards compatibility: because Nixpkgs has a rarely used
function named or, allow stuff like map or [...]. */ function named or, allow stuff like map or [...]. */
expr_simple OR_KW expr_simple OR_KW
{ $$ = new ExprApp(CUR_POS, $1, new ExprVar(CUR_POS, data->symbols.create("or"))); } { $$ = new ExprCall(CUR_POS, $1, {new ExprVar(CUR_POS, data->symbols.create("or"))}); }
| expr_simple { $$ = $1; } | expr_simple { $$ = $1; }
; ;
@ -412,10 +427,10 @@ expr_simple
} }
| SPATH { | SPATH {
string path($1 + 1, strlen($1) - 2); string path($1 + 1, strlen($1) - 2);
$$ = new ExprApp(CUR_POS, $$ = new ExprCall(CUR_POS,
new ExprApp(new ExprVar(data->symbols.create("__findFile")), new ExprVar(data->symbols.create("__findFile")),
new ExprVar(data->symbols.create("__nixPath"))), {new ExprVar(data->symbols.create("__nixPath")),
new ExprString(data->symbols.create(path))); new ExprString(data->symbols.create(path))});
} }
| URI { | URI {
static bool noURLLiterals = settings.isExperimentalFeatureEnabled(Xp::NoUrlLiterals); static bool noURLLiterals = settings.isExperimentalFeatureEnabled(Xp::NoUrlLiterals);