https://gcc.gnu.org/g:5869a881442aa4214d5deed7cfe0d352bcca1fd4
commit r15-9414-g5869a881442aa4214d5deed7cfe0d352bcca1fd4 Author: Patrick Palka <ppa...@redhat.com> Date: Sun Apr 13 11:04:46 2025 -0400 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 <ja...@redhat.com> Diff: --- 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..7e375823d648 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) + { + /* 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.