On Sat, 12 Apr 2025, Jason Merrill wrote:
> On 4/11/25 4:36 PM, Patrick Palka wrote:
> > Bootstrapped and regtested on x86_64-pc-linux-gnu, does this look OK for
> > GCC 15 perhaps?
> >
> > -- >8 --
> >
> > Here 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 constexpr 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.
> >
> > But the call doesn't depend on mce_value at all here (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 this case. Specifically if a
> > constexpr call with mce_unknown gets successfully evaluated, we can safely
> > reuse the result during a subsequent mce_true or mce_false evaluation.
> >
> > This patch implements this by 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 as the "in progress" result, so 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.
> > ---
> > gcc/cp/constexpr.cc | 59 +++++++++++++++++++++++++++------------------
> > 1 file changed, 35 insertions(+), 24 deletions(-)
> >
> > diff --git a/gcc/cp/constexpr.cc b/gcc/cp/constexpr.cc
> > index 0242425370f4..a4fd0d072f65 100644
> > --- a/gcc/cp/constexpr.cc
> > +++ b/gcc/cp/constexpr.cc
> > @@ -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)
>
> This looks like it will set the true/false results to the actual result even
> if we set the unknown result to error_mark_node because !cacheable?
The code path sets the true/false results by directly copying the
current (i.e. mce_unknown) result:
entry->result (mce_true) = entry->result (mce_unknown);
entry->result (mce_false) = entry->result (mce_unknown);
so it'll mirror whatever logic is used to set the unknow result.
>
> > + {
> > + /* Evaluation succeeded and was independent of whether we're in
> > a
> > + manifestly constant-evaluated context, so we can also reuse
> > + this result 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.
>
>