A call through a std::function whose target is fixed for the object's
lifetime is a two-level indirect dispatch: operator() calls the handler
stored in _M_invoker, and the handler calls the callable stored in the
_Any_data buffer. Early inlining may bring the stores to both fields and
the call into one function, but when the call sits in a loop, the stores
can be forwarded to the loads only if the dispatch calls do not write
the fields. The dispatch therefore stays indirect and blocks
vectorization of the loop. SPEC CPU 2026 772.marian_r has this shape in
its element-wise kernels when built with larger early-inlining limits
(--param early-inlining-insns=256).
Resolve such calls in ipa_analyze_node. For an indirect call through a
field of a local variable or by-value parameter, walk the virtual
definitions back from the load. Every path must store the same function
into the field, and no call in between may write it, including through
the store of its result. The target of each such call must be known. A
call through the field itself, as in a loop, is taken to call the stored
function, provided it loads the field on one of the paths the walk
checked. This makes the proof inductive. Calls through other fields are
resolved the same way. A target cannot write the field if its modref
stores, not counting its deferred calls, touch neither the field nor the
fields those calls go through, and if the targets of those calls write
no memory.
The call graph edge is then made direct, and inline_transform redirects
the statement as for any edge made direct during IPA. The function
pointers that the handler's deferred calls load are added to the edge's
aggregate jump function, so that IPA-CP or indirect inlining can make
the inner call direct. The inline summary accounts the outer call as a
direct call, as ipa_make_edge_direct_to_target does.
Because the target of a deferred call must write no memory and make no
deferred calls of its own, only chains of two levels, as in
std::function, resolve. A try/catch inside the loop also prevents the
proof, since __cxa_begin_catch and __cxa_end_catch have no modref
summary.
The new -fdevirtualize-function-pointers, enabled at -O3, gates both
the transformation and the modref recording of deferred calls.
Bootstrapped and regtested on aarch64-linux-gnu and x86_64-linux-gnu.
PR ipa/126241
gcc/ChangeLog:
* common.opt: Add -fdevirtualize-function-pointers.
* common.opt.urls: Regenerate.
* doc/invoke.texi: Document -fdevirtualize-function-pointers.
* ipa-fnsummary.cc (analyze_function_body): Account an indirect
call with a direct edge as a direct call.
* ipa-modref.cc (modref_access_analysis::analyze_call): Record
deferred calls only with -fdevirtualize-function-pointers.
* ipa-modref.h (modref_may_conflict): Declare.
* ipa-prop.cc (struct fnptr_slot): New.
(struct fnptr_ctx): New.
(fnptr_size): New.
(fnptr_slot_p): New.
(fnptr_init_deferred_slot): New.
(fnptr_same_slot_p): New.
(struct fnptr_walk_data): New.
(fnptr_walk_cb): New.
(fnptr_slot_value): New.
(fnptr_callee_preserves_ref_p): New.
(fnptr_call_target): New.
(ipa_devirt_local_fnptr_calls): New.
(ipa_record_local_fnptr_agg_values): New.
(ipa_analyze_node): Call them.
* opts.cc: Enable -fdevirtualize-function-pointers at -O3.
* tree-ssa-alias.cc (modref_may_conflict): Make non-static.
gcc/testsuite/ChangeLog:
* g++.dg/ipa/devirt-function-pointers.C: New test.
* g++.dg/lto/devirt-function-pointers_0.C: New test.
* gcc.dg/ipa/devirt-function-pointers-1.c: New test.
* gcc.dg/ipa/devirt-function-pointers-2.c: New test.
Signed-off-by: Konstantinos Eleftheriou <[email protected]>
---
gcc/common.opt | 4 +
gcc/common.opt.urls | 3 +
gcc/doc/invoke.texi | 13 +-
gcc/ipa-fnsummary.cc | 13 +
gcc/ipa-modref.cc | 2 +
gcc/ipa-modref.h | 2 +
gcc/ipa-prop.cc | 380 ++++++++++++++
gcc/opts.cc | 1 +
.../g++.dg/ipa/devirt-function-pointers.C | 144 ++++++
.../g++.dg/lto/devirt-function-pointers_0.C | 63 +++
.../gcc.dg/ipa/devirt-function-pointers-1.c | 462 ++++++++++++++++++
.../gcc.dg/ipa/devirt-function-pointers-2.c | 48 ++
gcc/tree-ssa-alias.cc | 2 +-
13 files changed, 1135 insertions(+), 2 deletions(-)
create mode 100644 gcc/testsuite/g++.dg/ipa/devirt-function-pointers.C
create mode 100644 gcc/testsuite/g++.dg/lto/devirt-function-pointers_0.C
create mode 100644 gcc/testsuite/gcc.dg/ipa/devirt-function-pointers-1.c
create mode 100644 gcc/testsuite/gcc.dg/ipa/devirt-function-pointers-2.c
diff --git a/gcc/common.opt b/gcc/common.opt
index c40606570e90..5badf4624d8c 100644
--- a/gcc/common.opt
+++ b/gcc/common.opt
@@ -1425,6 +1425,10 @@ fdevirtualize
Common Var(flag_devirtualize) Optimization
Try to convert virtual calls to direct ones.
+fdevirtualize-function-pointers
+Common Var(flag_devirtualize_function_pointers) Optimization
+Convert calls through function-pointer fields of local objects to direct calls.
+
fdiagnostics-show-location=
Common Joined RejectNegative Enum(diagnostic_prefixing_rule)
-fdiagnostics-show-location=[once|every-line] How often to emit source
location at the beginning of line-wrapped diagnostics.
diff --git a/gcc/common.opt.urls b/gcc/common.opt.urls
index d2baa15db8fc..b27ed185dfb6 100644
--- a/gcc/common.opt.urls
+++ b/gcc/common.opt.urls
@@ -806,6 +806,9 @@
UrlSuffix(gcc/Optimize-Options.html#index-fdevirtualize-speculatively)
fdevirtualize
UrlSuffix(gcc/Optimize-Options.html#index-fdevirtualize)
+fdevirtualize-function-pointers
+UrlSuffix(gcc/Optimize-Options.html#index-fdevirtualize-function-pointers)
+
fdiagnostics-show-location=
UrlSuffix(gcc/Diagnostic-Message-Formatting-Options.html#index-fdiagnostics-show-location)
diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
index ef9cb025c227..f0348c8ee2e4 100644
--- a/gcc/doc/invoke.texi
+++ b/gcc/doc/invoke.texi
@@ -615,6 +615,7 @@ Objective-C and Objective-C++ Dialects}.
-fcx-limited-range -fcx-method
-fdata-sections -fdce -fdelayed-branch
-fdelete-null-pointer-checks -fdep-fusion -fdevirtualize
+-fdevirtualize-function-pointers
-fdevirtualize-speculatively -fdevirtualize-at-ltrans -fdse
-fduplicate-switch-dispatch
-fearly-inlining -fexcess-precision=@var{style}
@@ -13706,7 +13707,8 @@ Optimize yet more. @option{-O3} turns on all
optimizations specified
by @option{-O2} and also turns on the following optimization flags:
@c Please keep the following list alphabetized!
-@gccoptlist{-fgcse-after-reload
+@gccoptlist{-fdevirtualize-function-pointers
+-fgcse-after-reload
-fipa-cp-clone
-floop-interchange
-floop-unroll-and-jam
@@ -14390,6 +14392,15 @@ indirect inlining (@option{-findirect-inlining}) and
interprocedural constant
propagation (@option{-fipa-cp}).
Enabled at levels @option{-O2}, @option{-O3}, @option{-Os}.
+@opindex fdevirtualize-function-pointers
+@opindex fno-devirtualize-function-pointers
+@item -fdevirtualize-function-pointers
+Attempt to convert calls through function pointers stored in fields of
+local objects, such as the dispatch of a @code{std::function}, to direct
+calls. This is done when every path to the call stores the same function
+into the field and the calls in between cannot change it.
+Enabled at @option{-O3}.
+
@opindex fdevirtualize-speculatively
@opindex fno-devirtualize-speculatively
@item -fdevirtualize-speculatively
diff --git a/gcc/ipa-fnsummary.cc b/gcc/ipa-fnsummary.cc
index d3225a084836..fd72998c50ae 100644
--- a/gcc/ipa-fnsummary.cc
+++ b/gcc/ipa-fnsummary.cc
@@ -3067,6 +3067,19 @@ analyze_function_body (struct cgraph_node *node, bool
early)
{
ipa_call_summary *es = ipa_call_summaries->get_create (edge);
+ /* An indirect call whose edge was already made direct is
+ accounted as the direct call it will become. */
+ if (!gimple_call_internal_p (stmt)
+ && !gimple_call_fndecl (stmt)
+ && !edge->indirect_unknown_callee
+ && !edge->speculative)
+ {
+ this_size -= (eni_size_weights.indirect_call_cost
+ - eni_size_weights.call_cost);
+ this_time -= (eni_time_weights.indirect_call_cost
+ - eni_time_weights.call_cost);
+ }
+
/* Special case: results of BUILT_IN_CONSTANT_P will be always
resolved as constant. We however don't want to optimize
out the cgraph edges. */
diff --git a/gcc/ipa-modref.cc b/gcc/ipa-modref.cc
index e9236af28954..3529d8b724f3 100644
--- a/gcc/ipa-modref.cc
+++ b/gcc/ipa-modref.cc
@@ -1707,6 +1707,8 @@ modref_access_analysis::analyze_call (gcall *stmt)
modref_deferred_call dc;
if (!m_ipa
&& !cfun->after_inlining
+ && opt_for_fn (current_function_decl,
+ flag_devirtualize_function_pointers)
&& deferred_dispatch_call_p (stmt, &dc))
{
if (m_skip_deferred)
diff --git a/gcc/ipa-modref.h b/gcc/ipa-modref.h
index 4d5051ea841e..d16c8cb1dd64 100644
--- a/gcc/ipa-modref.h
+++ b/gcc/ipa-modref.h
@@ -97,6 +97,8 @@ modref_summary *get_modref_function_summary (gcall *call,
bool *interposed);
void ipa_modref_cc_finalize ();
void ipa_merge_modref_summary_after_inlining (cgraph_edge *e);
bool ipa_modref_callee_reads_no_memory_p (gcall *call);
+/* In tree-ssa-alias.cc. */
+bool modref_may_conflict (const gcall *, modref_records *, ao_ref *, bool);
/* All flags that are implied by the ECF_CONST functions. */
static const int implicit_const_eaf_flags
diff --git a/gcc/ipa-prop.cc b/gcc/ipa-prop.cc
index 69628bf962a0..31546197af54 100644
--- a/gcc/ipa-prop.cc
+++ b/gcc/ipa-prop.cc
@@ -48,6 +48,8 @@ along with GCC; see the file COPYING3. If not see
#include "tree-dfa.h"
#include "tree-inline.h"
#include "ipa-fnsummary.h"
+#include "ipa-modref-tree.h"
+#include "ipa-modref.h"
#include "gimple-pretty-print.h"
#include "ipa-utils.h"
#include "dbgcnt.h"
@@ -3620,6 +3622,381 @@ ipa_release_body_info (struct ipa_func_body_info *fbi)
fbi->bb_infos.release ();
}
+/* Devirtualization of calls through function-pointer fields of local objects,
+ such as the two levels of dispatch of a std::function. A load from such a
+ field, a slot, yields function F when:
+ - the slot is an exactly accessed, non-volatile, pointer-sized field of a
+ local variable or by-value parameter,
+ - every path to the load stores &F into the slot, and only calls may write
+ it after that store,
+ - each of those calls has a known target, does not store its result over
+ the slot, and cannot write it. That is, the stores of the target,
+ ignoring its deferred calls, conflict with neither the slot nor the
+ fields those calls go through, and the targets of the deferred calls,
+ found the same way, write no memory.
+ A call through the slot itself is taken to call F, but only in the memory
+ states the walk to the stores visited, which makes the proof inductive. */
+
+/* A pointer-sized field of a local variable or by-value parameter. While the
+ field is assumed to hold a function, VALUE is that function and REGION the
+ virtual operands where the assumption holds. */
+
+struct fnptr_slot
+{
+ ao_ref ref;
+ tree value = NULL_TREE;
+ bitmap region = NULL;
+};
+
+/* The state of the analysis of one function: the slots assumed to hold their
+ values, the alias walk budget left for the whole function, and the edges
+ made direct. */
+
+struct fnptr_ctx
+{
+ fnptr_ctx (int steps) : budget (steps) {}
+ auto_vec<fnptr_slot> assumed;
+ int budget;
+ auto_vec<cgraph_edge *> done;
+};
+
+/* The size of a function pointer, which can exceed POINTER_SIZE. */
+
+static poly_int64
+fnptr_size ()
+{
+ return tree_to_poly_int64 (TYPE_SIZE (ptr_type_node));
+}
+
+/* Return true if SLOT is exactly a non-volatile, pointer-sized field of a
+ local variable or by-value parameter. */
+
+static bool
+fnptr_slot_p (fnptr_slot *slot)
+{
+ tree base = ao_ref_base (&slot->ref);
+ return (!slot->ref.volatile_p
+ && base
+ && auto_var_p (base)
+ && known_eq (slot->ref.size, fnptr_size ())
+ && known_eq (slot->ref.max_size, fnptr_size ()));
+}
+
+/* Fill SLOT with the field through which the callee of CALL makes deferred
+ call DC. Return false unless fnptr_slot_p accepts it. */
+
+static bool
+fnptr_init_deferred_slot (gcall *call, const modref_deferred_call &dc,
+ fnptr_slot *slot)
+{
+ if ((unsigned) dc.parm_index >= gimple_call_num_args (call))
+ return false;
+
+ tree arg = gimple_call_arg (call, dc.parm_index);
+ if (!POINTER_TYPE_P (TREE_TYPE (arg)))
+ return false;
+
+ ao_ref_init_from_ptr_and_range (&slot->ref, arg, true, dc.field_offset,
+ fnptr_size (), fnptr_size ());
+ return fnptr_slot_p (slot);
+}
+
+/* Return true if SLOT1 and SLOT2 are the same field. */
+
+static bool
+fnptr_same_slot_p (const fnptr_slot &slot1, const fnptr_slot &slot2)
+{
+ return (slot1.ref.base == slot2.ref.base
+ && known_eq (slot1.ref.offset, slot2.ref.offset));
+}
+
+/* The result of the walk of fnptr_slot_value: the function stored into the
+ slot, whether anything else may write it, and the calls on the way. */
+
+struct fnptr_walk_data
+{
+ tree value = NULL_TREE;
+ bool failed = false;
+ auto_vec<gcall *> calls;
+};
+
+/* Callback for walk_aliased_vdefs. Record a store of a function address into
+ REF, which ends the path, collect calls for later checking, and fail on
+ anything else. */
+
+static bool
+fnptr_walk_cb (ao_ref *ref, tree vdef, void *data_)
+{
+ fnptr_walk_data *data = (fnptr_walk_data *) data_;
+ gimple *stmt = SSA_NAME_DEF_STMT (vdef);
+ if (gcall *call = dyn_cast <gcall *> (stmt))
+ {
+ /* The summary of the callee does not cover the store to the result. */
+ if (gimple_store_p (call))
+ {
+ ao_ref lhs_ref;
+ ao_ref_init (&lhs_ref, gimple_call_lhs (call));
+ if (refs_may_alias_p_1 (ref, &lhs_ref, true))
+ {
+ data->failed = true;
+ return true;
+ }
+ }
+
+ data->calls.safe_push (call);
+ return false;
+ }
+
+ if (gimple_assign_single_p (stmt))
+ {
+ tree rhs = gimple_assign_rhs1 (stmt);
+ if (TREE_CODE (rhs) == ADDR_EXPR
+ && TREE_CODE (TREE_OPERAND (rhs, 0)) == FUNCTION_DECL
+ && stmt_kills_ref_p (stmt, ref))
+ {
+ tree fn = TREE_OPERAND (rhs, 0);
+ if (data->value && data->value != fn)
+ data->failed = true;
+ data->value = fn;
+ return true;
+ }
+ }
+
+ data->failed = true;
+ return true;
+}
+
+static tree fnptr_slot_value (fnptr_slot *, tree, fnptr_ctx &);
+
+/* Return true if a call to CALLEE cannot write REF, given the slots CTX
+ assumes. CALL is the call statement, or NULL when the arguments are
+ unknown, in which case CALLEE must write no memory at all. */
+
+static bool
+fnptr_callee_preserves_ref_p (tree callee, gcall *call, ao_ref *ref,
+ fnptr_ctx &ctx)
+{
+ if (flags_from_decl_or_type (callee) & (ECF_CONST | ECF_PURE))
+ return true;
+
+ cgraph_node *node = cgraph_node::get (callee);
+ modref_summary *summary = node ? get_modref_function_summary (node) : NULL;
+ if (!summary)
+ return false;
+
+ if (!call)
+ return (!summary->deferred_calls.length ()
+ && !summary->stores->every_base
+ && !vec_safe_length (summary->stores->bases));
+
+ modref_records *stores = summary->stores;
+ if (summary->deferred_calls.length ())
+ stores = summary->deferred_clean_stores;
+ if (!stores || modref_may_conflict (call, stores, ref, true))
+ return false;
+
+ for (const modref_deferred_call &dc : summary->deferred_calls)
+ {
+ /* The callee must not write the field, so that the deferred call goes
+ through the value the field has at CALL. */
+ fnptr_slot slot;
+ if (!fnptr_init_deferred_slot (call, dc, &slot)
+ || modref_may_conflict (call, stores, &slot.ref, true))
+ return false;
+
+ tree target = fnptr_slot_value (&slot, gimple_vuse (call), ctx);
+ if (!target || !fnptr_callee_preserves_ref_p (target, NULL, ref, ctx))
+ return false;
+ }
+
+ return true;
+}
+
+/* Return the function CALL invokes, given the slots CTX assumes, or
+ NULL_TREE if unknown. */
+
+static tree
+fnptr_call_target (gcall *call, fnptr_ctx &ctx)
+{
+ if (tree fndecl = gimple_call_fndecl (call))
+ return fndecl;
+
+ tree fn = gimple_call_fn (call);
+ if (!fn || TREE_CODE (fn) != SSA_NAME)
+ return NULL_TREE;
+
+ gimple *def = SSA_NAME_DEF_STMT (fn);
+ if (!gimple_assign_load_p (def))
+ return NULL_TREE;
+
+ fnptr_slot slot;
+ ao_ref_init (&slot.ref, gimple_assign_rhs1 (def));
+ if (!fnptr_slot_p (&slot))
+ return NULL_TREE;
+
+ return fnptr_slot_value (&slot, gimple_vuse (def), ctx);
+}
+
+/* Return the function that SLOT holds when read with virtual operand VUSE,
+ or NULL_TREE if the conditions above do not hold. While the calls on the
+ paths are checked, CTX assumes the function in the region of the walk. */
+
+static tree
+fnptr_slot_value (fnptr_slot *slot, tree vuse, fnptr_ctx &ctx)
+{
+ if (!vuse)
+ return NULL_TREE;
+
+ for (const fnptr_slot &assumed : ctx.assumed)
+ if (fnptr_same_slot_p (assumed, *slot))
+ return (bitmap_bit_p (assumed.region, SSA_NAME_VERSION (vuse))
+ ? assumed.value : NULL_TREE);
+
+ /* The proof for one slot can need the proof for another, as for the two
+ fields of a std::function called in a loop. Limit that nesting to four
+ slots, and stop once the walk budget is spent. */
+ if (ctx.assumed.length () >= 4 || ctx.budget <= 0)
+ return NULL_TREE;
+
+ fnptr_walk_data data;
+ bool entry_reached;
+ auto_bitmap region;
+ bitmap visited = region;
+ int walked = walk_aliased_vdefs (&slot->ref, vuse, fnptr_walk_cb, &data,
+ &visited, &entry_reached, ctx.budget);
+ if (walked < 0)
+ {
+ ctx.budget = 0;
+ return NULL_TREE;
+ }
+
+ ctx.budget -= walked;
+ if (data.failed || entry_reached || !data.value)
+ return NULL_TREE;
+
+ ctx.assumed.safe_push ({slot->ref, data.value, region});
+ bool ok = true;
+ for (gcall *call : data.calls)
+ {
+ tree callee = fnptr_call_target (call, ctx);
+ if (!callee
+ || !fnptr_callee_preserves_ref_p (callee, call, &slot->ref, ctx))
+ {
+ ok = false;
+ break;
+ }
+ }
+
+ ctx.assumed.pop ();
+ return ok ? data.value : NULL_TREE;
+}
+
+/* Make indirect calls of NODE through fixed function-pointer fields of local
+ variables or by-value parameters direct, and record the new direct edges
+ in CTX. */
+
+static void
+ipa_devirt_local_fnptr_calls (cgraph_node *node, fnptr_ctx &ctx)
+{
+ if (!opt_for_fn (node->decl, flag_devirtualize_function_pointers))
+ return;
+
+ cgraph_edge *next;
+ for (cgraph_edge *ie = node->indirect_calls; ie; ie = next)
+ {
+ next = ie->next_callee;
+ if (ie->speculative)
+ continue;
+
+ tree target = fnptr_call_target (ie->call_stmt, ctx);
+ if (!target)
+ continue;
+
+ cgraph_node *callee = cgraph_node::get (target);
+ if (!callee || callee->inlined_to || !dbg_cnt (devirt))
+ continue;
+
+ if (dump_file)
+ {
+ fprintf (dump_file, "ipa-prop: local fnptr devirt in %s to %s: ",
+ node->dump_name (), callee->dump_name ());
+ print_gimple_stmt (dump_file, ie->call_stmt, 0);
+ }
+
+ if (dump_enabled_p ())
+ dump_printf_loc (MSG_OPTIMIZED_LOCATIONS, ie->call_stmt,
+ "converting indirect call in %s to direct call "
+ "to %s\n", node->dump_name (), callee->dump_name ());
+ ctx.done.safe_push (cgraph_edge::make_direct (ie, callee));
+ }
+}
+
+/* For each edge that CTX made direct, add the known function pointers that
+ the deferred calls of its callee load to its jump functions. These are
+ the values at the call. The callee may change a field before loading it,
+ but IPA-CP and indirect inlining use an item only for a load of memory
+ that is unmodified since the function entry. */
+
+static void
+ipa_record_local_fnptr_agg_values (fnptr_ctx &ctx)
+{
+ for (cgraph_edge *cs : ctx.done)
+ {
+ ipa_edge_args *args = ipa_edge_args_sum->get (cs);
+ modref_summary *summary = get_modref_function_summary (cs->callee);
+ if (!args || !summary)
+ continue;
+
+ int max_items = opt_for_fn (cs->caller->decl, param_ipa_max_agg_items);
+ for (const modref_deferred_call &dc : summary->deferred_calls)
+ {
+ /* IPA-CP uses items only at non-negative, byte-aligned offsets below
+ UINT_MAX bytes. */
+ if (dc.field_offset < 0
+ || dc.field_offset % BITS_PER_UNIT != 0
+ || dc.field_offset >= (HOST_WIDE_INT) UINT_MAX * BITS_PER_UNIT
+ || dc.parm_index >= ipa_get_cs_argument_count (args))
+ continue;
+
+ ipa_jump_func *jf = ipa_get_ith_jump_func (args, dc.parm_index);
+ if (vec_safe_length (jf->agg.items) >= (unsigned) max_items)
+ continue;
+
+ /* Keep the items sorted by offset. */
+ unsigned idx = 0;
+ while (idx < vec_safe_length (jf->agg.items)
+ && (*jf->agg.items)[idx].offset < dc.field_offset)
+ idx++;
+ if (idx < vec_safe_length (jf->agg.items)
+ && (*jf->agg.items)[idx].offset == dc.field_offset)
+ continue;
+
+ fnptr_slot slot;
+ if (!fnptr_init_deferred_slot (cs->call_stmt, dc, &slot))
+ continue;
+
+ tree target = fnptr_slot_value (&slot, gimple_vuse (cs->call_stmt),
+ ctx);
+ if (!target)
+ continue;
+
+ ipa_agg_jf_item item;
+ tree addr = build_fold_addr_expr (target);
+ item.offset = dc.field_offset;
+ item.type = TREE_TYPE (addr);
+ item.jftype = IPA_JF_CONST;
+ item.value.constant = addr;
+ jf->agg.by_ref = true;
+ vec_safe_insert (jf->agg.items, idx, item);
+ if (dump_file)
+ fprintf (dump_file, "ipa-prop: local fnptr agg value for %s arg %i "
+ "offset " HOST_WIDE_INT_PRINT_DEC ": %s\n",
+ cs->callee->dump_name (), dc.parm_index, dc.field_offset,
+ cgraph_node::get (target)->dump_name ());
+ }
+ }
+}
+
/* Initialize the array describing properties of formal parameters
of NODE, analyze their uses and compute jump functions associated
with actual arguments of calls from within NODE. */
@@ -3650,6 +4027,8 @@ ipa_analyze_node (struct cgraph_node *node)
struct function *func = DECL_STRUCT_FUNCTION (node->decl);
push_cfun (func);
calculate_dominance_info (CDI_DOMINATORS);
+ fnptr_ctx fnptr (opt_for_fn (node->decl, param_ipa_max_aa_steps));
+ ipa_devirt_local_fnptr_calls (node, fnptr);
ipa_initialize_node_params (node);
ipa_analyze_controlled_uses (node);
@@ -3675,6 +4054,7 @@ ipa_analyze_node (struct cgraph_node *node)
enable_ranger (cfun, false);
analysis_dom_walker (&fbi).walk (ENTRY_BLOCK_PTR_FOR_FN (cfun));
disable_ranger (cfun);
+ ipa_record_local_fnptr_agg_values (fnptr);
ipa_release_body_info (&fbi);
free_dominance_info (CDI_DOMINATORS);
diff --git a/gcc/opts.cc b/gcc/opts.cc
index ec98edc9a515..ed533e194c13 100644
--- a/gcc/opts.cc
+++ b/gcc/opts.cc
@@ -701,6 +701,7 @@ static const struct default_options default_options_table[]
=
/* -O3 and -Os optimizations. */
/* -O3 optimizations. */
+ { OPT_LEVELS_3_PLUS, OPT_fdevirtualize_function_pointers, NULL, 1 },
{ OPT_LEVELS_3_PLUS, OPT_fgcse_after_reload, NULL, 1 },
{ OPT_LEVELS_3_PLUS, OPT_fipa_cp_clone, NULL, 1 },
{ OPT_LEVELS_3_PLUS, OPT_floop_interchange, NULL, 1 },
diff --git a/gcc/testsuite/g++.dg/ipa/devirt-function-pointers.C
b/gcc/testsuite/g++.dg/ipa/devirt-function-pointers.C
new file mode 100644
index 000000000000..27ca8af34617
--- /dev/null
+++ b/gcc/testsuite/g++.dg/ipa/devirt-function-pointers.C
@@ -0,0 +1,144 @@
+// PR ipa/126241
+// { dg-do run { target c++11 } }
+// { dg-options "-O3 -fdump-ipa-cp -fdump-tree-optimized" }
+// { dg-skip-if "requires hosted libstdc++ for functional function" { !
hostedlib } }
+
+// Both levels of a std::function dispatch are resolved when the local
+// object holds a known callable, and only then. A call through a volatile
+// function-pointer field is never resolved.
+
+#include <functional>
+
+struct V { float v[4]; };
+
+struct Of {
+ static float fma2 (const float &a, const float &b) { return a * b + (a + b);
}
+ static float add (const float &a, const float &b) { return a + b; }
+ static float mul (const float &a, const float &b) { return a * b; }
+};
+
+static double g (double a, double b) { return a * 100.0 + b; }
+
+typedef std::function<float (const float &, const float &)> Fn;
+typedef std::function<float (float, float)> FnConv;
+
+template <typename F>
+static inline V
+loop4 (const F &f, const V &x, const V &y)
+{
+ V o;
+ for (int i = 0; i < 4; i++)
+ o.v[i] = f (x.v[i], y.v[i]);
+ return o;
+}
+
+// flatten brings the construction of the std::function temporary and the
+// dispatch loop into one function.
+__attribute__((noinline, flatten)) void
+elem (const V *a, const V *b, V *o, int n)
+{
+ for (int i = 0; i < n; i++)
+ o[i] = loop4<Fn> (Of::fma2, a[i], b[i]);
+}
+
+// A local std::function, which needs no flatten.
+__attribute__((noinline)) void
+elem2 (const V *a, const V *b, V *o, int n)
+{
+ Fn f = Of::fma2;
+ for (int i = 0; i < n; i++)
+ for (int j = 0; j < 4; j++)
+ o[i].v[j] = f (a[i].v[j], b[i].v[j]);
+}
+
+// g takes doubles, so the handler converts the arguments before calling it.
+__attribute__((noinline, flatten)) V
+conv (const V &x, const V &y)
+{
+ return loop4<FnConv> (g, x, y);
+}
+
+// f is constructed with Of::add, then conditionally reassigned to Of::mul.
+static inline V
+cond (Fn f, const V &x, const V &y, int sw)
+{
+ if (sw)
+ f = Of::mul;
+ return loop4 (f, x, y);
+}
+
+__attribute__((noinline, flatten)) void
+run_cond (const V *a, const V *b, V *o, int n, int sw)
+{
+ for (int i = 0; i < n; i++)
+ o[i] = cond (Of::add, a[i], b[i], sw);
+}
+
+// f is constructed with Of::add, then overwritten by a copy of other.
+static inline V
+copy (Fn f, Fn other, const V &x, const V &y)
+{
+ f = other;
+ return loop4 (f, x, y);
+}
+
+__attribute__((noinline, flatten)) void
+run_copy (const V *a, const V *b, V *o, int n)
+{
+ for (int i = 0; i < n; i++)
+ o[i] = copy (Of::add, Of::mul, a[i], b[i]);
+}
+
+// A load of a volatile field may not read what was stored into it.
+struct VolOp { float (*volatile fn) (const float &, const float &); };
+
+__attribute__((noinline)) void
+run_vol (const V *a, const V *b, V *o, int n)
+{
+ VolOp op;
+ op.fn = Of::add;
+ for (int i = 0; i < n; i++)
+ for (int j = 0; j < 4; j++)
+ o[i].v[j] = op.fn (a[i].v[j], b[i].v[j]);
+}
+
+static void
+check (const V &o, const V &a, const V &b, float (*fn) (float, float))
+{
+ for (int i = 0; i < 4; i++)
+ if (o.v[i] != fn (a.v[i], b.v[i]))
+ __builtin_abort ();
+}
+
+int
+main ()
+{
+ V a = {{ 1, 2, 3, 4 }}, b = {{ 5, 6, 7, 8 }}, o;
+
+ elem (&a, &b, &o, 1);
+ check (o, a, b, [] (float x, float y) { return x * y + (x + y); });
+ elem2 (&a, &b, &o, 1);
+ check (o, a, b, [] (float x, float y) { return x * y + (x + y); });
+ o = conv (a, b);
+ check (o, a, b,
+ [] (float x, float y)
+ { return (float) ((double) x * 100.0 + (double) y); });
+ run_cond (&a, &b, &o, 1, 1);
+ check (o, a, b, [] (float x, float y) { return x * y; });
+ run_cond (&a, &b, &o, 1, 0);
+ check (o, a, b, [] (float x, float y) { return x + y; });
+ run_copy (&a, &b, &o, 1);
+ check (o, a, b, [] (float x, float y) { return x * y; });
+ run_vol (&a, &b, &o, 1);
+ check (o, a, b, [] (float x, float y) { return x + y; });
+ return 0;
+}
+
+// { dg-final { scan-ipa-dump-times "local fnptr agg value \[^\n\]*Of::fma2" 2
"cp" } }
+// { dg-final { scan-ipa-dump "local fnptr agg value \[^\n\]*double
g\\(double, double\\)" "cp" } }
+// { dg-final { scan-ipa-dump-not "local fnptr agg value
\[^\n\]*Of::(add|mul)" "cp" } }
+// { dg-final { scan-ipa-dump-not "local fnptr devirt in \[^\n\]*run_vol" "cp"
} }
+
+// No call through an SSA name is left in the section of elem, elem2 or conv,
+// or of their clones, in the optimized dump.
+// { dg-final { scan-tree-dump-not ";; Function (elem|elem2|conv)(\\.\[^ \]*)?
\[^\n\]*\n(\[^;\n\]\[^\n\]*\n|\n)* (\[^\n\]* = )?\[A-Za-z_.\]*_\[0-9\]+ \\("
"optimized" } }
diff --git a/gcc/testsuite/g++.dg/lto/devirt-function-pointers_0.C
b/gcc/testsuite/g++.dg/lto/devirt-function-pointers_0.C
new file mode 100644
index 000000000000..f7d9b159fb92
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/devirt-function-pointers_0.C
@@ -0,0 +1,63 @@
+// PR ipa/126241
+// { dg-lto-do run }
+// { dg-lto-options {{-O3 -flto -fdump-tree-optimized}} }
+// { dg-skip-if "requires hosted libstdc++ for functional function" { !
hostedlib } }
+
+// The call graph edge made direct at compile time is streamed while its
+// statement is still indirect. Check the results of elem, and that no call
+// through an SSA name is left in it at link time.
+
+#include <functional>
+
+struct V { float v[4]; };
+
+static float
+fma2 (const float &a, const float &b)
+{
+ return a * b + (a + b);
+}
+
+static inline V
+loop4 (const std::function<float (const float &, const float &)> &f,
+ const V &x, const V &y)
+{
+ V o;
+ for (int i = 0; i < 4; i++)
+ o.v[i] = f (x.v[i], y.v[i]);
+ return o;
+}
+
+// flatten brings the construction of the std::function temporary and the
+// dispatch loop into elem.
+__attribute__((noinline, flatten)) void
+elem (const V *a, const V *b, V *o, int n)
+{
+ for (int i = 0; i < n; i++)
+ o[i] = loop4 (fma2, a[i], b[i]);
+}
+
+int
+main ()
+{
+ V a[8], b[8], o[8];
+ for (int i = 0; i < 8; i++)
+ for (int j = 0; j < 4; j++)
+ {
+ a[i].v[j] = i + j;
+ b[i].v[j] = i - j;
+ o[i].v[j] = -1.0f;
+ }
+ elem (a, b, o, 8);
+ for (int i = 0; i < 8; i++)
+ for (int j = 0; j < 4; j++)
+ {
+ float x = i + j, y = i - j;
+ if (o[i].v[j] != x * y + (x + y))
+ __builtin_abort ();
+ }
+ return 0;
+}
+
+// No call through an SSA name is left in the section of elem, or of its
+// clones, in the optimized dump at link time.
+// { dg-final { scan-ltrans-tree-dump-not ";; Function elem(\\.\[^ \]*)?
\[^\n\]*\n(\[^;\n\]\[^\n\]*\n|\n)* (\[^\n\]* = )?\[A-Za-z_.\]*_\[0-9\]+ \\("
"optimized" } }
diff --git a/gcc/testsuite/gcc.dg/ipa/devirt-function-pointers-1.c
b/gcc/testsuite/gcc.dg/ipa/devirt-function-pointers-1.c
new file mode 100644
index 000000000000..146c8e990d88
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/ipa/devirt-function-pointers-1.c
@@ -0,0 +1,462 @@
+/* PR ipa/126241 */
+/* { dg-do run } */
+/* { dg-options "-O3 -fdump-ipa-cp -fdump-tree-optimized" } */
+/* { dg-add-options bind_pic_locally } */
+
+/* Calls through function-pointer fields of local structures are resolved at
+ -O3 when every path stores the same function and nothing in between can
+ change it, and only then. */
+
+/* The handler stores its result through another pointer, which must not be
+ mistaken for a store to the field. */
+typedef int (*stores_inner) (int);
+struct stores { stores_inner inner; void (*h) (struct stores *, int, int *); };
+
+__attribute__((noinline)) int stores_sq (int x) { return x * x; }
+
+__attribute__((noinline)) void
+stores_handler (struct stores *p, int x, int *out)
+{
+ *out = p->inner (x);
+}
+
+__attribute__((noinline)) void
+stores_run (int *o, int n)
+{
+ struct stores s;
+ int i;
+ s.inner = stores_sq;
+ s.h = stores_handler;
+ for (i = 0; i < n; i++)
+ s.h (&s, i, &o[i]);
+}
+
+/* A call on the way writes another field of the object. */
+struct field;
+typedef int (*field_fn) (struct field *);
+struct field { field_fn fn; int cnt; };
+
+__attribute__((noinline)) int field_one (struct field *p) { return 1; }
+__attribute__((noinline)) void field_bump (struct field *p) { p->cnt++; }
+
+__attribute__((noinline)) int
+field_run (int n)
+{
+ struct field s;
+ int i, r = 0;
+ s.fn = field_one;
+ s.cnt = 0;
+ for (i = 0; i < n; i++)
+ {
+ field_bump (&s);
+ r += s.fn (&s);
+ }
+ return r + s.cnt;
+}
+
+/* The first call through INV is made direct earlier, and the aggregate jump
+ function of the second call already has an item for Y. The item for FN
+ must be inserted before it. */
+struct sorted { int (*fn) (int); int (*inv) (struct sorted *, int); int y; };
+
+static int sorted_f1 (int v) { return v + 1; }
+
+__attribute__((noinline)) static int
+sorted_handler (struct sorted *s, int v)
+{
+ return s->fn (v);
+}
+
+__attribute__((noinline)) int
+sorted_run (int n)
+{
+ struct sorted s;
+ int r;
+ s.fn = sorted_f1;
+ s.inv = sorted_handler;
+ r = s.inv (&s, n);
+ s.y = 6;
+ return r + s.inv (&s, n) + s.y;
+}
+
+/* None of the calls in the remaining cases except few_run may be
+ devirtualized. */
+
+/* The target of FP is loaded before the store that the proof starts
+ from. */
+struct early;
+typedef int (*early_fn) (struct early *);
+struct early { early_fn fn; };
+
+__attribute__((noinline)) int early_one (struct early *s) { return 1; }
+__attribute__((noinline)) int early_two (struct early *s) { return 2; }
+
+__attribute__((noinline)) int
+early_writer (struct early *s)
+{
+ s->fn = early_two;
+ return 0;
+}
+
+__attribute__((noipa)) void
+early_init (struct early *s)
+{
+ s->fn = early_writer;
+}
+
+__attribute__((noinline)) int
+early_run (void)
+{
+ struct early s;
+ early_fn fp;
+ early_init (&s);
+ fp = s.fn;
+ s.fn = early_one;
+ fp (&s);
+ return s.fn (&s);
+}
+
+/* As early_run, but the early call, through H in nested_call_h, is only
+ reached while proving INNER for the call in nested_call_inner. */
+struct nested;
+typedef void (*nested_inner) (struct nested *);
+typedef int (*nested_h) (struct nested *);
+struct nested { nested_inner inner; nested_h h; };
+
+__attribute__((noinline)) int nested_one (struct nested *p) { return 1; }
+__attribute__((noinline)) int nested_two (struct nested *p) { return 2; }
+__attribute__((noinline)) void nested_good (struct nested *p) { }
+
+__attribute__((noinline)) void
+nested_evil (struct nested *p)
+{
+ p->h = nested_two;
+}
+
+__attribute__((noinline)) int
+nested_writer (struct nested *p)
+{
+ p->inner = nested_evil;
+ return 0;
+}
+
+__attribute__((noipa)) void
+nested_init (struct nested *p)
+{
+ p->h = nested_writer;
+}
+
+__attribute__((noinline)) void nested_call_h (struct nested *p) { p->h (p); }
+
+__attribute__((noinline)) void
+nested_call_inner (struct nested *p)
+{
+ p->inner (p);
+}
+
+__attribute__((noinline)) int
+nested_run (void)
+{
+ struct nested s;
+ nested_init (&s);
+ s.inner = nested_good;
+ nested_call_h (&s);
+ s.h = nested_one;
+ nested_call_inner (&s);
+ return s.h (&s);
+}
+
+/* The handler rewrites the field it calls through. */
+struct writes;
+typedef void (*writes_inner) (struct writes *);
+typedef int (*writes_h) (struct writes *);
+struct writes { writes_inner inner; writes_h h; };
+
+__attribute__((noinline)) int writes_one (struct writes *p) { return 1; }
+__attribute__((noinline)) int writes_two (struct writes *p) { return 2; }
+__attribute__((noinline)) void writes_good (struct writes *p) { }
+
+__attribute__((noinline)) void
+writes_evil (struct writes *p)
+{
+ p->h = writes_two;
+}
+
+__attribute__((noinline)) void
+writes_set_evil (struct writes *p)
+{
+ p->inner = writes_evil;
+}
+
+__attribute__((noinline)) void
+writes_handler (struct writes *p)
+{
+ writes_set_evil (p);
+ p->inner (p);
+}
+
+__attribute__((noinline)) int
+writes_run (void)
+{
+ struct writes s;
+ s.inner = writes_good;
+ s.h = writes_one;
+ writes_handler (&s);
+ return s.h (&s);
+}
+
+/* The handler calls through a variably indexed field. */
+struct indexed;
+typedef void (*indexed_inner) (struct indexed *);
+typedef int (*indexed_h) (struct indexed *);
+struct indexed { indexed_inner arr[2]; indexed_h h; };
+
+__attribute__((noinline)) int indexed_one (struct indexed *p) { return 1; }
+__attribute__((noinline)) int indexed_two (struct indexed *p) { return 2; }
+__attribute__((noinline)) void indexed_good (struct indexed *p) { }
+
+__attribute__((noinline)) void
+indexed_evil (struct indexed *p)
+{
+ p->h = indexed_two;
+}
+
+__attribute__((noinline)) void
+indexed_handler (struct indexed *p, int i)
+{
+ p->arr[i] (p);
+}
+
+__attribute__((noinline)) int
+indexed_run (int i)
+{
+ struct indexed s;
+ s.arr[0] = indexed_good;
+ s.arr[1] = indexed_evil;
+ s.h = indexed_one;
+ indexed_handler (&s, i);
+ return s.h (&s);
+}
+
+/* The call that overwrites the object is the store to its result. */
+typedef int (*lhs_fn) (void);
+struct lhs { lhs_fn fn; };
+
+__attribute__((noinline)) int lhs_one (void) { return 1; }
+__attribute__((noinline)) int lhs_two (void) { return 2; }
+
+__attribute__((noinline)) struct lhs
+lhs_make (void)
+{
+ struct lhs r;
+ r.fn = lhs_two;
+ return r;
+}
+
+__attribute__((noinline)) int
+lhs_peek (struct lhs *p)
+{
+ return p->fn == lhs_one;
+}
+
+__attribute__((noinline)) int
+lhs_run (void)
+{
+ struct lhs s;
+ int r;
+ s.fn = lhs_one;
+ r = lhs_peek (&s);
+ s = lhs_make ();
+ return r + s.fn ();
+}
+
+/* The handler calls itself, and the recursive invocation calls through the
+ field of another object. */
+struct rec;
+typedef void (*rec_fn) (struct rec *);
+typedef int (*rec_h) (struct rec *);
+struct rec { rec_fn fn; rec_h h; struct rec *next; struct rec *back; };
+
+__attribute__((noinline)) int rec_one (struct rec *p) { return 1; }
+__attribute__((noinline)) int rec_two (struct rec *p) { return 2; }
+__attribute__((noinline)) void rec_good (struct rec *p) { }
+
+__attribute__((noinline)) void
+rec_evil (struct rec *p)
+{
+ p->back->h = rec_two;
+}
+
+__attribute__((noinline)) void
+rec_walk (struct rec *p)
+{
+ if (p->next)
+ rec_walk (p->next);
+ p->fn (p);
+}
+
+__attribute__((noinline)) int
+rec_run (void)
+{
+ struct rec s, t;
+ t.fn = rec_evil;
+ t.next = 0;
+ t.back = &s;
+ s.fn = rec_good;
+ s.next = &t;
+ s.h = rec_one;
+ rec_walk (&s);
+ return s.h (&s);
+}
+
+/* The target of the handler's deferred call writes the field called after
+ the handler returns. */
+struct target;
+typedef void (*target_inner) (struct target *);
+typedef int (*target_h) (struct target *);
+struct target { target_inner inner; target_h h; };
+
+__attribute__((noinline)) int target_one (struct target *p) { return 1; }
+__attribute__((noinline)) int target_two (struct target *p) { return 2; }
+
+__attribute__((noinline)) void
+target_evil (struct target *p)
+{
+ p->h = target_two;
+}
+
+__attribute__((noinline)) void
+target_handler (struct target *p)
+{
+ p->inner (p);
+}
+
+__attribute__((noinline)) int
+target_run (void)
+{
+ struct target s;
+ s.inner = target_evil;
+ s.h = target_one;
+ target_handler (&s);
+ return s.h (&s);
+}
+
+/* Two paths store different functions. The call after each store keeps
+ the stores apart. */
+struct conflict;
+typedef int (*conflict_fn) (struct conflict *);
+struct conflict { conflict_fn fn; };
+
+volatile int conflict_vol;
+
+__attribute__((noinline)) int conflict_one (struct conflict *p) { return 1; }
+__attribute__((noinline)) int conflict_two (struct conflict *p) { return 2; }
+__attribute__((noinline)) void conflict_note (void) { (void) conflict_vol; }
+
+__attribute__((noinline)) int
+conflict_run (int c, int n)
+{
+ struct conflict s;
+ int i, r = 0;
+ if (c)
+ {
+ s.fn = conflict_one;
+ conflict_note ();
+ }
+ else
+ {
+ s.fn = conflict_two;
+ conflict_note ();
+ }
+ for (i = 0; i < n; i++)
+ r += s.fn (&s);
+ return r;
+}
+
+/* On the path without a store, the field of the by-value parameter holds the
+ function of the caller. */
+struct parm;
+typedef int (*parm_fn) (struct parm *);
+struct parm { parm_fn fn; };
+
+__attribute__((noinline)) int parm_one (struct parm *p) { return 1; }
+__attribute__((noinline)) int parm_two (struct parm *p) { return 2; }
+
+__attribute__((noinline)) int
+parm_run (struct parm s, int c, int n)
+{
+ int i, r = 0;
+ if (c)
+ s.fn = parm_one;
+ for (i = 0; i < n; i++)
+ r += s.fn (&s);
+ return r;
+}
+
+/* The handlers take more arguments than the calls pass. The calls never
+ run, but may be devirtualized and inlined. */
+struct few;
+typedef void (*few_inner) (struct few *);
+struct few { few_inner inner; void (*h) (void); void (*h2) (struct few *); };
+
+__attribute__((noinline)) void few_good (struct few *p) { }
+void few_handler (struct few *q) { q->inner (q); }
+
+void
+few_handler2 (struct few *p, struct few *q)
+{
+ q->inner (q);
+}
+
+__attribute__((noinline)) void few_call_inner (struct few *p) { p->inner (p); }
+
+__attribute__((noinline)) void
+few_run (int c)
+{
+ struct few s;
+ s.inner = few_good;
+ s.h = (void (*) (void)) few_handler;
+ s.h2 = (void (*) (struct few *)) few_handler2;
+ few_call_inner (&s);
+ if (c == 1)
+ s.h ();
+ else if (c == 2)
+ s.h2 (&s);
+}
+
+int
+main (void)
+{
+ int o[4];
+ struct parm ps = { parm_two };
+
+ stores_run (o, 4);
+ if (o[3] != 9
+ || field_run (3) != 6
+ || sorted_run (1) != 10
+ || early_run () != 2
+ || nested_run () != 2
+ || writes_run () != 2
+ || indexed_run (1) != 2
+ || lhs_run () != 3
+ || rec_run () != 2
+ || target_run () != 2
+ || conflict_run (0, 2) != 4
+ || conflict_run (1, 2) != 2
+ || parm_run (ps, 0, 2) != 4
+ || parm_run (ps, 1, 2) != 2)
+ __builtin_abort ();
+ few_run (0);
+ return 0;
+}
+
+/* { dg-final { scan-ipa-dump "local fnptr devirt in stores_run/\[0-9\]+ to
stores_handler/" "cp" } } */
+/* { dg-final { scan-ipa-dump "local fnptr agg value \[^\n\]*stores_sq/" "cp"
} } */
+/* { dg-final { scan-ipa-dump "local fnptr agg value \[^\n\]*sorted_f1/" "cp"
} } */
+/* { dg-final { scan-ipa-dump "local fnptr devirt in field_run/\[0-9\]+ to
field_one/" "cp" } } */
+/* { dg-final { scan-ipa-dump-not "local fnptr devirt in
(early|nested|writes|indexed|lhs|rec|target|conflict|parm)_run/" "cp" } } */
+
+/* No call through an SSA name is left in the sections of stores_run and
+ field_run or of their clones, or of the clones of stores_handler, in the
+ optimized dump. */
+/* { dg-final { scan-tree-dump-not ";; Function (stores_run(\\.\[^
\]*)?|stores_handler\\.\[^ \]+|field_run(\\.\[^ \]*)?)
\[^\n\]*\n(\[^;\n\]\[^\n\]*\n|\n)* (\[^\n\]* = )?\[A-Za-z_.\]*_\[0-9\]+ \\("
"optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/ipa/devirt-function-pointers-2.c
b/gcc/testsuite/gcc.dg/ipa/devirt-function-pointers-2.c
new file mode 100644
index 000000000000..6136f02c0168
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/ipa/devirt-function-pointers-2.c
@@ -0,0 +1,48 @@
+/* PR ipa/126241 */
+/* { dg-do compile { target lp64 } } */
+/* { dg-options "-O3 -fdump-ipa-cp" } */
+
+/* IPA-CP stores aggregate offsets as unsigned int byte counts, so no item
+ may be recorded for the deferred call through FN, at byte offset 2^32.
+ Truncated, that offset would be 0, the offset of X. */
+
+typedef long (*big_fnptr) (long);
+struct big;
+typedef long (*big_inv) (struct big *, long);
+struct big
+{
+ big_fnptr x;
+ char pad[(1UL << 32) - sizeof (big_fnptr)];
+ big_fnptr fn;
+ big_inv inv;
+};
+
+static long big_fn (long val) { return val + 1; }
+
+__attribute__((noinline)) static long
+big_x (long val)
+{
+ return val * 3;
+}
+
+__attribute__((noinline)) static long
+big_handler (struct big *obj, long val)
+{
+ return obj->x (val) + obj->fn (val);
+}
+
+long
+big_run (long val, int count)
+{
+ struct big obj;
+ long sum = 0;
+ obj.x = big_x;
+ obj.fn = big_fn;
+ obj.inv = big_handler;
+ for (int iter = 0; iter < count; iter++)
+ sum += obj.inv (&obj, val);
+ return sum;
+}
+
+/* { dg-final { scan-ipa-dump "local fnptr agg value \[^\n\]*offset 0: big_x/"
"cp" } } */
+/* { dg-final { scan-ipa-dump-not "local fnptr agg value \[^\n\]*big_fn/" "cp"
} } */
diff --git a/gcc/tree-ssa-alias.cc b/gcc/tree-ssa-alias.cc
index 41072b4d5d9b..224b4938a203 100644
--- a/gcc/tree-ssa-alias.cc
+++ b/gcc/tree-ssa-alias.cc
@@ -2646,7 +2646,7 @@ refs_output_dependent_p (tree store1, tree store2)
/* Returns true if and only if REF may alias any access stored in TT.
IF TBAA_P is true, use TBAA oracle. */
-static bool
+bool
modref_may_conflict (const gcall *stmt,
modref_tree <alias_set_type> *tt, ao_ref *ref, bool tbaa_p)
{
--
2.55.0