c++: improve constexpr call caching [PR115639]

For the testcase from this PR, checking

  static_assert(0 == big_calc());

takes twice as much time as

  constexpr int ret = big_calc();
  static_assert(0 == ret);

ultimately because in the former, we first constant evaluate big_calc()
with mce_unknown (as part of warning-dependent folding from
cp_build_binary_op).  We then constant evaluate it a second time, with
mce_true, during finish_static_assert.  The result of the first
evaluation isn't reused because of the different mce_value, which in
general can give a different result.

But big_calc() here doesn't depend on mce_value at all (i.e. there's no if
consteval or __builtin_is_constant_evaluated calls, nested or otherwise)
so we should be able to reuse the result in such cases.  Specifically if a
constexpr call with mce_unknown succeeds, we can safely reuse the result
during a subsequent mce_true or mce_false evaluation.

This patch implements this by also caching a successful mce_unknown call
result into the corresponding mce_true and mce_false slots, so that such
a subsequent evaluation effectively reuses the mce_unknown result.  To
make it more convenient to access the cache slot for the same call with
different mce_value, this patch gives each constexpr_call entry three
result slots, one per mce_value, instead of having a distinct
constexpr_call entry for each mce_value.  And we can no longer use
NULL_TREE to denote the call is in progress; instead use unknown_type_node.

After this patch compile time for the above two fragments is the same.

	PR c++/115639

gcc/cp/ChangeLog:

	* constexpr.cc (struct constexpr_call): Add NSDMIs to each
	field.  Replace 'result' data member with 3-element 'results'
	array and a 'result' accessor function.  Remove
	'manifestly_const_eval' data member.
	(constexpr_call_hasher::equal): Adjust after constexpr_call
	layout change.
	(cxx_eval_call_expression): Likewise.  Define some local
	variables closer to their first use.  Use unknown_type_node
	instead of NULL_TREE as the "in progress" result.  After
	successully evaluating a call with mce_unknown, also cache the
	result in the corresponding mce_true and mce_false slots.

Reviewed-by: Jason Merrill <jason@redhat.com>
This commit is contained in:
Patrick Palka 2025-04-13 11:04:46 -04:00
parent 962c3d69f1
commit 5869a88144

View file

@ -1119,20 +1119,22 @@ explain_invalid_constexpr_fn (tree fun)
struct GTY((for_user)) constexpr_call {
/* Description of the constexpr function definition. */
constexpr_fundef *fundef;
constexpr_fundef *fundef = nullptr;
/* Parameter bindings environment. A TREE_VEC of arguments. */
tree bindings;
/* Result of the call.
NULL means the call is being evaluated.
tree bindings = NULL_TREE;
/* Result of the call, indexed by the value of
constexpr_ctx::manifestly_const_eval.
unknown_type_node means the call is being evaluated.
error_mark_node means that the evaluation was erroneous or otherwise
uncacheable (e.g. because it depends on the caller).
Otherwise, the actual value of the call. */
tree result;
tree results[3] = { NULL_TREE, NULL_TREE, NULL_TREE };
/* The hash of this call; we remember it here to avoid having to
recalculate it when expanding the hash table. */
hashval_t hash;
/* The value of constexpr_ctx::manifestly_const_eval. */
enum mce_value manifestly_const_eval;
hashval_t hash = 0;
/* The result slot corresponding to the given mce_value. */
tree& result (mce_value mce) { return results[1 + int(mce)]; }
};
struct constexpr_call_hasher : ggc_ptr_hash<constexpr_call>
@ -1427,8 +1429,6 @@ constexpr_call_hasher::equal (constexpr_call *lhs, constexpr_call *rhs)
return true;
if (lhs->hash != rhs->hash)
return false;
if (lhs->manifestly_const_eval != rhs->manifestly_const_eval)
return false;
if (!constexpr_fundef_hasher::equal (lhs->fundef, rhs->fundef))
return false;
return cp_tree_equal (lhs->bindings, rhs->bindings);
@ -2855,9 +2855,6 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
{
location_t loc = cp_expr_loc_or_input_loc (t);
tree fun = get_function_named_in_call (t);
constexpr_call new_call
= { NULL, NULL, NULL, 0, ctx->manifestly_const_eval };
int depth_ok;
if (fun == NULL_TREE)
return cxx_eval_internal_function (ctx, t, lval,
@ -3099,7 +3096,6 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
if (DECL_IMMEDIATE_FUNCTION_P (fun))
{
new_ctx.manifestly_const_eval = mce_true;
new_call.manifestly_const_eval = mce_true;
ctx = &new_ctx;
}
@ -3112,6 +3108,7 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
|| cp_noexcept_operand);
bool non_constant_args = false;
constexpr_call new_call;
new_call.bindings
= cxx_bind_parameters_in_call (ctx, t, fun, non_constant_p,
overflow_p, &non_constant_args);
@ -3185,7 +3182,7 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
}
}
depth_ok = push_cx_call_context (t);
int depth_ok = push_cx_call_context (t);
/* Remember the object we are constructing or destructing. */
tree new_obj = NULL_TREE;
@ -3227,8 +3224,6 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
new_call.hash = constexpr_fundef_hasher::hash (new_call.fundef);
new_call.hash
= iterative_hash_template_arg (new_call.bindings, new_call.hash);
new_call.hash
= iterative_hash_object (ctx->manifestly_const_eval, new_call.hash);
/* If we have seen this call before, we are done. */
maybe_initialize_constexpr_call_table ();
@ -3246,22 +3241,23 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
the slot can move during evaluation of the body. */
*slot = entry = ggc_alloc<constexpr_call> ();
*entry = new_call;
entry->result (ctx->manifestly_const_eval) = unknown_type_node;
fb.preserve ();
}
}
/* Calls that are in progress have their result set to NULL, so that we
can detect circular dependencies. Now that we only cache up to
constexpr_cache_depth this won't catch circular dependencies that
/* Calls that are in progress have their result set to unknown_type_node,
so that we can detect circular dependencies. Now that we only cache
up to constexpr_cache_depth this won't catch circular dependencies that
start deeper, but they'll hit the recursion or ops limit. */
else if (entry->result == NULL)
else if (entry->result (ctx->manifestly_const_eval) == unknown_type_node)
{
if (!ctx->quiet)
error ("call has circular dependency");
*non_constant_p = true;
entry->result = result = error_mark_node;
entry->result (ctx->manifestly_const_eval) = result = error_mark_node;
}
else
result = entry->result;
result = entry->result (ctx->manifestly_const_eval);
}
if (!depth_ok)
@ -3482,7 +3478,22 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
else if (!result)
result = void_node;
if (entry)
entry->result = cacheable ? result : error_mark_node;
{
entry->result (ctx->manifestly_const_eval)
= cacheable ? result : error_mark_node;
if (result != error_mark_node
&& ctx->manifestly_const_eval == mce_unknown)
{
/* Evaluation succeeded and was independent of whether we're in a
manifestly constant-evaluated context, so we can also reuse
this result when evaluating this call with a fixed context. */
if (!entry->result (mce_true))
entry->result (mce_true) = entry->result (mce_unknown);
if (!entry->result (mce_false))
entry->result (mce_false) = entry->result (mce_unknown);
}
}
}
/* The result of a constexpr function must be completely initialized.