[PATCH v2 1/3] Add a pass to devirtualize polymorphic PMF calls

Chi Wang <[email protected]>
Newsgroups gmane.comp.gcc.patches
Message-ID <[email protected]>
Add a tree pass gimple-ptrmemfunc to convert trivial polymorphic call
via PMF to a direct target. This pass identifies the pattern when a PMF
call is reduced to the virtual branch by folding and/or constant
propagation. The branch identification is based on a vtable indexing and
a vtable load from an object pointer tagged with IFN_PTRMEMFUNC_OBJ. For
instance, in

  struct X { /* ...  */ };
  struct Y : P, X { /* ...  */ };
  struct Z : Q, Y { /* ...  */ };
  Z* obj;
  void (Y::* func) ();
  (obj->*func) ();

The PMF call is through two adjustments: one via front end casting Z* to
Y* (suppose it is 16 bytes), and another, if func is a virtual method of
X, then another *this adjustment is made (suppose it is 8 bytes). The
generated GIMPLE will be:

  _7 = &obj_16(D)->D.2726;
  _6 = .PTRMEMFUNC_OBJ (_7, 0B, 16B);
  _8 = MEM[(int (*) () * *)_6 + 8B];
  _10 = *_8;
  _11 = &obj_16(D)->D.2726;
  _12 = _11 + 8B;
  _10 (_12);

where 0B is typed pointer to method type void (Y::*) (Y*), and 16B is
typed Z*, essentially preserving the front end cast information. Here
the second argument is 0B because Z to Y is not through any virtual
base; it is 1B if otherwise.

The pass will walk from the call _10, find the index is 0 bytes into the
vtable, and the vtable is loaded from the secondary vtable of _6 (offset
by 24 bytes from type Z). It will then extract type information from the
PTRMEMFUNC_OBJ internal call, and look up the exact function entry in
that vtable.

If the function is final or the type is final, the call is made direct.
This is analogous to how the C++ FE emits a direct call when it is known
to be final. In addition, if the vtable lookup is out of bounds on
secondary or primary final vtable, the index is invalid and it changes
the call to __builtin_unreachable. If no definite direct target exists,
we emit an OBJ_TYPE_REF token to reuse the existing polymorphic call
devirtualization. In all recognized cases, where the offsets are
constants, the IFN_PTRMEMFUNC_OBJ is removed.

The pass is run twice, once before IPA and after early inline to
discover PMF direct calls, and once after IPA. Each time it only scans
functions marked with PENDING_TODO_ptrmemfunc_devirt, which is currently
set by tree-ssa-forwprop.

This pass also adjusts the thunk property propagation to also process
DECL_FINAL_P. This enables the pass to still inspect the property in the
middle end.

To identify a PMF call, the C++ front end attaches the PMF marker
IFN_PTRMEMFUNC_OBJ onto the virtual branch. This internal function wraps
the object pointer and is a no-op when converted to RTL. Technically
this can obscure an indirect call data flow with internal function, but
it should not be a problem since the data flow from the
IFN_PTRMEMFUNC_OBJ only propagates to the polymorphic method lookup,
and once a target exists, the pass will transform the call into a
direct or polymorphic call and remove the marker, so it should have no
effect on that.

gcc/ChangeLog:

	* Makefile.in: Add gimple-ptrmemfunc.o.
	* internal-fn.cc (expand_PTRMEMFUNC_OBJ): Expand
	IFN_PTRMEMFUNC_OBJ.
	* internal-fn.def (PTRMEMFUNC_OBJ): Define
	IFN_PTRMEMFUNC_OBJ.
	* doc/ifn.texi: Document it.
	* passes.def: Add two gimple-ptrmemfunc passes.
	* timevar.def (TV_GIMPLE_PTRMEMFUNC): Add timevar for gimple-
	ptrmemfunc.
	* gimple-ptrmemfunc.cc: New file.
	* gimple-ptrmemfunc.h: New file.
	* tree-pass.h (make_pass_ptrmemfunc): Make gimple-ptrmemfunc
	pass.
	(PENDING_TODO_ptrmemfunc_devirt): Add flag for gimple-ptrmemfunc
	to run for a function.
	* tree-ssa-forwprop.cc (pass_forwprop::execute): Add flag when
	IFN_PTRMEMFUNC_OBJ is found in a function.

gcc/cp/ChangeLog:

	* constexpr.cc (cxx_eval_internal_function): Consider
	IFN_PTRMEMFUNC_OBJ as a no-op constexpr function.
	(potential_constant_expression_1): Propagate through
	IFN_PTRMEMFUNC_OBJ.
	* method.cc (make_thunk): Make thunk also inherit DECL_FINAL_P
	from the function.
	* typeck.cc (get_member_function_from_ptrfunc): Attach
	IFN_PTRMEMFUNC_OBJ marker to the virtual branch of a PMF call.

gcc/testsuite/ChangeLog:

	* g++.dg/opt/devirtpmf1.C: New test.
	* g++.dg/opt/devirtpmf2.C: New test.
	* g++.dg/opt/devirtpmf3.C: New test.
	* g++.dg/opt/devirtpmf4.C: New test.
	* g++.dg/opt/devirtpmf5.C: New test.

---
> Preparatory work can be done as part of another pass that does general
> folding, for example like forwprop.  We should avoid repeated full IL walks
> for each and every tiny pattern that might eventually appear somewhere.

Added a PENDING_TODO_ptrmemfunc_devirt to prevent walking IL of every function.
The flag is currently set by forwprop on function level. I am uncertain if it is
possible to flag, or keep a working list of basic blocks or statements, but
previous passes seem to change them a lot and I don't find existing places to
handle them.

Patch 2/3 and 3/3 are unchanged. Bootstrapped and regression tested for the
full series on C and C++ on x86_64-pc-linux-gnu, on unix and unix/-m32.

---
 gcc/Makefile.in                       |   1 +
 gcc/cp/constexpr.cc                   |   2 +
 gcc/cp/method.cc                      |   1 +
 gcc/cp/typeck.cc                      |  32 +-
 gcc/doc/ifn.texi                      |   5 +
 gcc/gimple-ptrmemfunc.cc              | 662 ++++++++++++++++++++++++++
 gcc/gimple-ptrmemfunc.h               |  29 ++
 gcc/internal-fn.cc                    |  11 +
 gcc/internal-fn.def                   |   3 +
 gcc/passes.def                        |   6 +
 gcc/testsuite/g++.dg/opt/devirtpmf1.C | 128 +++++
 gcc/testsuite/g++.dg/opt/devirtpmf2.C |  63 +++
 gcc/testsuite/g++.dg/opt/devirtpmf3.C |  57 +++
 gcc/testsuite/g++.dg/opt/devirtpmf4.C |  45 ++
 gcc/testsuite/g++.dg/opt/devirtpmf5.C |  72 +++
 gcc/timevar.def                       |   1 +
 gcc/tree-pass.h                       |   3 +
 gcc/tree-ssa-forwprop.cc              |   4 +
 18 files changed, 1119 insertions(+), 6 deletions(-)
 create mode 100644 gcc/gimple-ptrmemfunc.cc
 create mode 100644 gcc/gimple-ptrmemfunc.h
 create mode 100644 gcc/testsuite/g++.dg/opt/devirtpmf1.C
 create mode 100644 gcc/testsuite/g++.dg/opt/devirtpmf2.C
 create mode 100644 gcc/testsuite/g++.dg/opt/devirtpmf3.C
 create mode 100644 gcc/testsuite/g++.dg/opt/devirtpmf4.C
 create mode 100644 gcc/testsuite/g++.dg/opt/devirtpmf5.C

diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index ee2f9022eab..40af4ad44e4 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1531,6 +1531,7 @@ OBJS = \
 	gimple-lower-bitint.o \
 	gimple-predicate-analysis.o \
 	gimple-pretty-print.o \
+	gimple-ptrmemfunc.o \
 	gimple-range.o \
 	gimple-range-cache.o \
 	gimple-range-edge.o \
diff --git a/gcc/cp/constexpr.cc b/gcc/cp/constexpr.cc
index e16c878d725..e6ccd308f69 100644
--- a/gcc/cp/constexpr.cc
+++ b/gcc/cp/constexpr.cc
@@ -3253,6 +3253,7 @@ cxx_eval_internal_function (const constexpr_ctx *ctx, tree t,
       break;
 
     case IFN_LAUNDER:
+    case IFN_PTRMEMFUNC_OBJ:
       return cxx_eval_constant_expression (ctx, CALL_EXPR_ARG (t, 0),
 					   vc_prvalue, non_constant_p,
 					   overflow_p, jump_target);
@@ -12080,6 +12081,7 @@ potential_constant_expression_1 (tree t, bool want_rval, bool strict, bool now,
 		case IFN_VEC_CONVERT:
 		case IFN_BSWAP:
 		case IFN_BITREVERSE:
+		case IFN_PTRMEMFUNC_OBJ:
 		  bail = false;
 		  break;
 
diff --git a/gcc/cp/method.cc b/gcc/cp/method.cc
index bf882b9619c..3c755d8377c 100644
--- a/gcc/cp/method.cc
+++ b/gcc/cp/method.cc
@@ -106,6 +106,7 @@ make_thunk (tree function, bool this_adjusting,
   DECL_LANG_SPECIFIC (thunk) = DECL_LANG_SPECIFIC (function);
   cxx_dup_lang_specific_decl (thunk);
   DECL_VIRTUAL_P (thunk) = true;
+  DECL_FINAL_P (thunk) = DECL_FINAL_P (function);
   SET_DECL_THUNKS (thunk, NULL_TREE);
 
   DECL_CONTEXT (thunk) = DECL_CONTEXT (function);
diff --git a/gcc/cp/typeck.cc b/gcc/cp/typeck.cc
index 5bde051b9b3..808694b3ec7 100644
--- a/gcc/cp/typeck.cc
+++ b/gcc/cp/typeck.cc
@@ -4505,24 +4505,44 @@ get_member_function_from_ptrfunc (tree *instance_ptrptr, tree function,
       if (e1 == error_mark_node)
 	return error_mark_node;
 
+      /* Before adding delta, compute the wrapper tokens.  */
+      tree outer_type = TREE_TYPE (TREE_TYPE (instance_ptr));
+      tree outer_type_token;
+      tree method_type_token = build_int_cst (fntype, 0);
       /* Convert down to the right base before using the instance.  A
 	 special case is that in a pointer to member of class C, C may
 	 be incomplete.  In that case, the function will of course be
 	 a member of C, and no conversion is required.  In fact,
 	 lookup_base will fail in that case, because incomplete
 	 classes do not have BINFOs.  */
-      if (!same_type_ignoring_top_level_qualifiers_p
-	  (basetype, TREE_TYPE (TREE_TYPE (instance_ptr))))
+      if (!same_type_ignoring_top_level_qualifiers_p (basetype, outer_type))
 	{
-	  basetype = lookup_base (TREE_TYPE (TREE_TYPE (instance_ptr)),
-				  basetype, ba_check, NULL, complain);
+	  base_kind bk;
+	  basetype
+	    = lookup_base (outer_type, basetype, ba_check, &bk, complain);
 	  instance_ptr = build_base_path (PLUS_EXPR, instance_ptr, basetype,
 					  1, complain);
 	  if (instance_ptr == error_mark_node)
 	    return error_mark_node;
+	  outer_type_token = fold_convert (build_pointer_type (outer_type),
+					   BINFO_OFFSET (basetype));
+	  method_type_token = build_int_cst (fntype, bk == bk_via_virtual);
 	}
-      /* ...and then the delta in the PMF.  */
+      else
+	{
+	  outer_type_token = build_int_cst (build_pointer_type (outer_type), 0);
+	  method_type_token = build_int_cst (fntype, 0);
+	}
+      /* Wrap the base converted object IFN_PTRMEMFUNC_OBJ.  */
+      tree wrapped_inst_ptr
+	= build_call_expr_internal_loc (input_location, IFN_PTRMEMFUNC_OBJ,
+					TREE_TYPE (instance_ptr), 3,
+					instance_ptr, method_type_token,
+					outer_type_token);
+      /* ...and then the delta in the PMF.  Do this separately so we will not
+	 let IFN_PTRMEMFUNC_OBJ slip out of this branch.  */
       instance_ptr = fold_build_pointer_plus (instance_ptr, delta);
+      wrapped_inst_ptr = fold_build_pointer_plus (wrapped_inst_ptr, delta);
 
       /* Hand back the adjusted 'this' argument to our caller.  */
       *instance_ptrptr = instance_ptr;
@@ -4533,7 +4553,7 @@ get_member_function_from_ptrfunc (tree *instance_ptrptr, tree function,
 
       /* Next extract the vtable pointer from the object.  */
       vtbl = build1 (NOP_EXPR, build_pointer_type (vtbl_ptr_type_node),
-		     instance_ptr);
+		     wrapped_inst_ptr);
       vtbl = cp_build_fold_indirect_ref (vtbl);
       if (vtbl == error_mark_node)
 	return error_mark_node;
diff --git a/gcc/doc/ifn.texi b/gcc/doc/ifn.texi
index 7cfc80eecf2..2aa17495b76 100644
--- a/gcc/doc/ifn.texi
+++ b/gcc/doc/ifn.texi
@@ -1546,6 +1546,11 @@ The @code{IFN_FLOATTOBITINT} internal function is expanded by the
 The @code{IFN_BITINTTOFLOAT} internal function is expanded by the
 @code{expand_BITINTTOFLOAT} function.
 
+@cindex @code{IFN_PTRMEMFUNC_OBJ} internal function
+@item @samp{IFN_PTRMEMFUNC_OBJ}
+The @code{IFN_PTRMEMFUNC_OBJ} internal function is expanded by the
+@code{expand_PTRMEMFUNC_OBJ} function.
+
 @cindex @code{IFN_VARYING} internal function
 @item @samp{IFN_VARYING}
 Use @code{IFN_VARYING} as a temporary placeholder for a value whose
diff --git a/gcc/gimple-ptrmemfunc.cc b/gcc/gimple-ptrmemfunc.cc
new file mode 100644
index 00000000000..6c53a4befb7
--- /dev/null
+++ b/gcc/gimple-ptrmemfunc.cc
@@ -0,0 +1,662 @@
+/* Optimize polymorphic constant pointer to member function calls.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+/* PMF = pointer-to-member function.
+   For each virtual branch of a PMF call,
+
+   (obj->*member)();
+
+   the call can be either a simple indirect or a polymorphic call.  When MEMBER
+   is later found to be a constant, we can apply optimization according to the
+   type of the call.  For the polymorphic case, suppose OBJ is typed O*, and
+   member is B::*, then there are two type conversions: front end casts OBJ from
+   O* to B*, and an implicit type conversion happens when PMF delta is applied
+   to adjust *this (which is typed B*).  In the following context, we refer to
+   O as the outer type, B as the base type, and the final adjusted *this type as
+   the inner type.  Because GIMPLE pointer type may be unreliable, C++ FE will
+   attach an object marker via an internal function call:
+
+   .PTRMEMFUNC_OBJ (obj, method_type_token, outer_type_token)
+
+   where
+   1 OBJ is the casted C++ PMF member function calling object pointer.
+   2 METHOD_TYPE_TOKEN has the type of pointer to the calling method type.
+   3 OUTER_TYPE_TOKEN has pointer to the outer type.
+   4 METHOD_TYPE_TOKEN is 0 or 1: whether the outer to base conversion is
+     through a virtual base cast.  This is equivalent to whether the offset
+     described by the next entry is valid for every derived outer type.
+   5 OUTER_TYPE_TOKEN has the value of the *this adjustment offset from outer
+     type to base type.
+
+   Based on the information, the pass can therefore identify the polymorphic
+   call case, since OBJ is now known to point to an object that is a subobject
+   of a known offset inside type O instead of an arbitrary pointer.
+
+   Note that, in many cases, inner type is a (possibly ambiguous or unreachable)
+   base of the outer type, but it is also possible that inner is a derived class
+   of outer class, or entirely unrelated.  For example:
+
+   struct A { virtual void foo (); };
+   struct B { virtual void bar (); };
+   struct C : A, B {};
+
+   B* b;
+   (b->*(static_cast<void (B::*) ()>
+	  (static_cast<void (C::*) ()> (&A::foo)))) ();
+
+   is a perfectly valid call provided that the runtime type of *b is always at
+   least C*.  In this case it is not possible to have a meaningful polymorphic
+   context to devirtualize.
+
+   This file implements a pass to optimize constant polymorphic PMFs that can
+   resolve to direct targets.  It also exposes helpers to match the
+   PTRMEMFUNC_OBJ call pattern as well as other direct target lookup functions
+   for IPA.
+*/
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "backend.h"
+#include "tree.h"
+#include "gimple.h"
+#include "tree-pass.h"
+#include "stringpool.h"
+#include "gimple-ssa.h"
+#include "tree-ssanames.h"
+#include "cgraph.h"
+#include "gimple-pretty-print.h"
+#include "tree-cfg.h"
+#include "tree-cfgcleanup.h"
+#include "tree-ssa-dce.h"
+#include "tree-dfa.h"
+#include "ipa-utils.h"
+#include "gimple-iterator.h"
+#include "gimplify-me.h"
+#include "gimple-fold.h"
+#include "fold-const.h"
+#include "gimple-ptrmemfunc.h"
+#include "dbgcnt.h"
+#include "langhooks.h"
+
+/* Matches virtual branch of call through pointer to member function.  The
+   folded GIMPLE should roughly be:
+
+   _2 = .PTRMEMFUNC_OBJ (_1, 0B, 0B);
+   _3 = MEM[(int (*) () * *)_2 + vt_offset];
+   _4 = MEM[_3 + vt_index];
+
+   where _4 is the returned function pointer.  If it is the virtual branch of a
+   ptrmemfunc_call, returns true, adds vt_offset to OFFSET and vt_index to
+   BYTE_IDX if they are non-null, and set VT to be the SSA of the extracted
+   vtable and PMF_OBJ to be the SSA in the LHS of IFN_PTRMEMFUNC_OBJ call.  If
+   BYTE_IDX and OFFSET are supplied, their corresponding component must also
+   fit into poly_int64.  The passed arguments might be modified even though the
+   function returns false.  */
+
+static bool
+gimple_analyze_virtual_ptrmemfunc_branch (tree vfn, tree &vt, tree &pmf_obj,
+					  poly_int64 *byte_idx,
+					  poly_int64 *offset)
+{
+  auto peel
+    = [] (gimple *ssa_def, enum tree_code code, tree &base, poly_int64 *offset)
+      {
+	tree base_op, off_op;
+	if (!is_gimple_assign (ssa_def)
+	    || gimple_assign_rhs_code (ssa_def) != code)
+	  return false;
+
+	if (code == MEM_REF)
+	  {
+	    base_op = TREE_OPERAND (gimple_assign_rhs1 (ssa_def), 0);
+	    off_op = TREE_OPERAND (gimple_assign_rhs1 (ssa_def), 1);
+	  }
+	else if (code == POINTER_PLUS_EXPR)
+	  {
+	    base_op = gimple_assign_rhs1 (ssa_def);
+	    off_op = gimple_assign_rhs2 (ssa_def);
+	  }
+	else
+	  gcc_unreachable ();
+
+	if (TREE_CODE (base_op) != SSA_NAME)
+	  return false;
+	if (offset)
+	  {
+	    if (!poly_int_tree_p (off_op))
+	      return false;
+	    poly_offset_int wide
+	      = poly_offset_int::from (wi::to_poly_wide (off_op), SIGNED);
+	    poly_int64 d;
+	    if (!wide.to_shwi (&d))
+	      return false;
+	    *offset += d;
+	  }
+	base = base_op;
+	return true;
+    };
+
+  gimple *vfn_def = SSA_NAME_DEF_STMT (vfn);
+  /* If target uses descriptors, expect a cast instead of a MEM_REF.  */
+  if (TARGET_VTABLE_USES_DESCRIPTORS)
+  {
+    if (is_gimple_assign (vfn_def)
+	&& CONVERT_EXPR_CODE_P (gimple_assign_rhs_code (vfn_def)))
+      {
+	vt = gimple_assign_rhs1 (vfn_def);
+	if (TREE_CODE (vt) != SSA_NAME)
+	  return false;
+      }
+    else
+      vt = vfn;
+  }
+  else if (!peel (vfn_def, MEM_REF, vt, byte_idx))
+    return false;
+  peel (SSA_NAME_DEF_STMT (vt), POINTER_PLUS_EXPR, vt, byte_idx);
+
+  gimple *vt_def = SSA_NAME_DEF_STMT (vt);
+  if (!peel (vt_def, MEM_REF, pmf_obj, offset))
+    return false;
+  peel (SSA_NAME_DEF_STMT (pmf_obj), POINTER_PLUS_EXPR, pmf_obj, offset);
+
+  gimple *pmf_obj_def = SSA_NAME_DEF_STMT (pmf_obj);
+  if (!gimple_call_internal_p (pmf_obj_def, IFN_PTRMEMFUNC_OBJ)
+      || gimple_call_num_args (pmf_obj_def) != 3)
+    return false;
+  return true;
+}
+
+bool
+gimple_analyze_virtual_ptrmemfunc_branch (tree vfn, tree &pmf_obj)
+{
+  tree vt;
+  return gimple_analyze_virtual_ptrmemfunc_branch (vfn, vt, pmf_obj, NULL,
+						   NULL);
+}
+
+/* Return a complete type of the outermost base subobject that begins at byte
+   OFFSET inside TYPE (TYPE itself when OFFSET is 0), or NULL_TREE
+   if no base subobject starts exactly at OFFSET.  */
+
+tree
+find_outermost_base_at_offset (tree type, HOST_WIDE_INT offset)
+{
+  HOST_WIDE_INT delta = offset * BITS_PER_UNIT;
+  while (true)
+    {
+      if (in_lto_p)
+	type = prevailing_odr_type (type);
+      if (!COMPLETE_TYPE_P (type))
+	return NULL;
+
+      HOST_WIDE_INT pos, size;
+      tree fld;
+
+      if (delta < 0)
+	return NULL_TREE;
+      if (delta == 0)
+	return type;
+      for (fld = TYPE_FIELDS (type); fld; fld = DECL_CHAIN (fld))
+	{
+	  if (TREE_CODE (fld) != FIELD_DECL || !DECL_ARTIFICIAL (fld)
+	      || TREE_CODE (TREE_TYPE (fld)) != RECORD_TYPE)
+	    continue;
+
+	  pos = int_bit_position (fld);
+	  size = tree_to_uhwi (DECL_SIZE (fld));
+	  if (known_in_range_p (delta, pos, size))
+	    break;
+	}
+      if (!fld)
+	return NULL_TREE;
+
+      type = TREE_TYPE (fld);
+      delta -= pos;
+    }
+}
+
+static bool
+is_virtual_fndecl_direct (tree &fn, tree type, bool is_secondary)
+{
+  /* Primary vtable with final type or secondary vtable cannot be extended.  */
+  if (fndecl_built_in_p (fn, BUILT_IN_UNREACHABLE, BUILT_IN_UNREACHABLE_TRAP))
+    return is_secondary || TYPE_FINAL_P (type);
+  /* Or, we land on a dtor.  It is not possible to call dtor from PMF.  */
+  if (DECL_CXX_DESTRUCTOR_P (fn))
+    {
+      fn = builtin_decl_unreachable ();
+      return true;
+    }
+
+  /* Check type is final or the method is final.  */
+  return TYPE_FINAL_P (type)
+	 || (TREE_CODE (TREE_TYPE (fn)) == METHOD_TYPE && DECL_FINAL_P (fn));
+}
+
+/* Return the polymorphic target of type INNER located at type BASE at offset
+   OFFSET at vtable index IDX if the target can be turned into a direct call,
+   or impossible for a PMF polymorphic call.  */
+tree
+get_ptrmemfunc_polymorphic_direct_target (tree base, tree inner,
+					  poly_int64 offset, HOST_WIDE_INT idx)
+{
+  if (in_lto_p)
+    base = prevailing_odr_type (base);
+  if (TYPE_BINFO (base) == NULL)
+    return NULL_TREE;
+  tree binfo = get_binfo_at_offset (TYPE_BINFO (base), offset, inner);
+  if (!binfo)
+    return NULL_TREE;
+
+  bool can_refer = false;
+  tree fndecl = gimple_get_virt_method_for_binfo (idx, binfo, &can_refer);
+  if (dump_file)
+    {
+      fprintf (dump_file, "Looking up virtual function *(base + ");
+      print_dec (offset, dump_file, SIGNED);
+      fprintf (dump_file, " bits)[" HOST_WIDE_INT_PRINT_DEC "] on type ", idx);
+      print_generic_expr (dump_file, base, TDF_SLIM);
+      fprintf (dump_file, " gives ");
+      print_generic_expr (dump_file, fndecl, TDF_SLIM);
+      fprintf (dump_file, can_refer ? ", can refer" : ", cannot refer");
+    }
+  if (!fndecl || !can_refer)
+    {
+      if (dump_file)
+	fprintf (dump_file, "\n");
+      return NULL_TREE;
+    }
+
+  bool is_direct
+    = is_virtual_fndecl_direct (fndecl, base, known_ne (offset, 0));
+  if (dump_file)
+    fprintf (dump_file, ", %s direct\n", is_direct ? "is" : "may not be");
+  return is_direct ? fndecl : NULL_TREE;
+}
+
+namespace {
+
+struct pass_ptrmemfunc_func_info
+{
+  hash_map<gimple *, gimple *> subst;
+  auto_vec<gimple *> unreachables;
+  bitmap dead_ssas;
+  bitmap dead_eh;
+
+  pass_ptrmemfunc_func_info ()
+    : dead_ssas (BITMAP_ALLOC (NULL)), dead_eh (BITMAP_ALLOC (NULL))
+  {}
+
+  ~pass_ptrmemfunc_func_info ()
+  {
+    BITMAP_FREE (dead_ssas);
+    BITMAP_FREE (dead_eh);
+  }
+};
+
+/* Matches the case when a pointer to member function call loses its non-virtual
+   branch.  If both the vtable index and the *this adjustment offset are
+   constants, optimize the call.
+
+   If the call is a direct final call or an impossible call, change the call
+   target directly.  Otherwise, emit an OBJ_TYPE_REF into the call:
+
+   OBJ_TYPE_REF(_4;_1(D)->vt_index) (_5, ...);
+
+   The vtable load will also be changed to bypass the PTRMEMFUNC_OBJ marker.
+   The marker should be DCE-ed when all dependent PMF calls are optimized or
+   with OTR token attached.  */
+
+void
+fold_virtual_ptrmemfunc_call (gcall *call, pass_ptrmemfunc_func_info &info)
+{
+  tree vfn = gimple_call_fn (call);
+  if (!vfn || TREE_CODE (vfn) != SSA_NAME)
+    return;
+
+  poly_int64 byte_idx_poly = 0, offset_poly = 0;
+  tree vt, pmf_obj;
+  if (!gimple_analyze_virtual_ptrmemfunc_branch (vfn, vt, pmf_obj,
+						 &byte_idx_poly, &offset_poly))
+    return;
+  if (!byte_idx_poly.is_constant () || !offset_poly.is_constant ())
+    return;
+  if (dump_file)
+    fprintf (dump_file, "\nFound constant polymorphic call from PMF\n");
+
+  gimple *pmf_obj_def = SSA_NAME_DEF_STMT (pmf_obj);
+  HOST_WIDE_INT byte_idx = byte_idx_poly.to_constant ();
+  HOST_WIDE_INT offset = offset_poly.to_constant ();
+  tree method_type = TREE_TYPE (TREE_TYPE (gimple_call_arg (pmf_obj_def, 1)));
+  tree base_type = TYPE_METHOD_BASETYPE (method_type);
+
+  if (dump_file)
+    {
+      fprintf (dump_file, "Constant PMF based on type ");
+      print_generic_expr (dump_file, base_type, TDF_SLIM);
+      fprintf (dump_file, "\n");
+    }
+
+  /* Find the vtable array index and try to resolve into a direct target.  */
+  unsigned HOST_WIDE_INT vt_entry_size
+    = tree_to_uhwi (TYPE_SIZE_UNIT (TREE_TYPE (vt)));
+  HOST_WIDE_INT idx = -1;
+  if (byte_idx < 0 || vt_entry_size == 0)
+    return;
+
+  bool give_up = false;
+  tree fndecl = NULL_TREE;
+  tree inner_type = NULL;
+  if (byte_idx % vt_entry_size != 0)
+    {
+      if (dump_file)
+	{
+	  fprintf (dump_file,
+		   "byte index " HOST_WIDE_INT_PRINT_DEC
+		   " is not divisible by the vtable size "
+		   HOST_WIDE_INT_PRINT_DEC "\n",
+		   byte_idx, vt_entry_size);
+	}
+      fndecl = builtin_decl_unreachable ();
+    }
+  else
+    {
+      idx = byte_idx / vt_entry_size;
+
+      /* See if we can find a method in the TYPE_BINFO of the inner type.
+	 We try to use the outer type if it is usable.  Since we only produce a
+	 target when it is final or unreachable, we don't care whether the outer
+	 to base cast is through any virtual base.  */
+      tree outer_type_token = gimple_call_arg (pmf_obj_def, 2);
+      if (tree_fits_poly_int64_p (outer_type_token))
+	{
+	  tree outer_type = TREE_TYPE (TREE_TYPE (outer_type_token));
+	  auto outer_offset_poly = tree_to_poly_int64 (outer_type_token);
+	  outer_offset_poly += offset_poly;
+	  if (known_lt (outer_offset_poly, 0))
+	    give_up = true;
+	  else if (outer_offset_poly.is_constant ())
+	    {
+	      inner_type
+		= find_outermost_base_at_offset
+		    (outer_type, outer_offset_poly.to_constant ());
+	    }
+	  outer_offset_poly *= BITS_PER_UNIT;
+	  if (inner_type)
+	    fndecl = get_ptrmemfunc_polymorphic_direct_target
+		       (outer_type, inner_type, outer_offset_poly, idx);
+	}
+      else
+	{
+	  if (offset < 0)
+	    give_up = true;
+	  else
+	    inner_type = find_outermost_base_at_offset (base_type, offset);
+	  if (inner_type)
+	    fndecl = get_ptrmemfunc_polymorphic_direct_target
+		       (base_type, inner_type, offset_poly, idx);
+	}
+    }
+
+  if (!fndecl && !inner_type && !give_up)
+    return;
+  if (inner_type && dump_file)
+    {
+      fprintf (dump_file, "Calling subobject type ");
+      print_generic_expr (dump_file, inner_type, TDF_SLIM);
+      fprintf (dump_file, "\n");
+    }
+
+  if (!dbg_cnt (devirt))
+    return;
+
+  /* If the call is direct, change the calling target and type of the function
+     if necessary.  Otherwise, we emit an OTR so it is recognized as a virtual
+     call.  */
+  bitmap_set_bit (info.dead_ssas, SSA_NAME_VERSION (vt));
+  bitmap_set_bit (info.dead_ssas, SSA_NAME_VERSION (vfn));
+  bitmap_set_bit (info.dead_ssas, SSA_NAME_VERSION (pmf_obj));
+
+  if (fndecl
+      && fndecl_built_in_p (fndecl, BUILT_IN_UNREACHABLE,
+			    BUILT_IN_UNREACHABLE_TRAP))
+    {
+      if (dump_file)
+	fprintf (dump_file,
+		 "Impossible call, giving __builtin_unreachable ()\n");
+      if (dump_enabled_p ())
+	{
+	  dump_printf_loc (MSG_OPTIMIZED_LOCATIONS, call,
+			   "converting virtual pointer-to-member call to "
+			   "__builtin_unreachable\n");
+	}
+      gimple_stmt_iterator gsi = gsi_for_stmt (call);
+      gimple *new_call
+	= gimple_build_builtin_unreachable (gimple_location (call));
+      info.unreachables.safe_push (new_call);
+      if (gsi_replace (&gsi, new_call, true))
+	bitmap_set_bit (info.dead_eh, gimple_bb (new_call)->index);
+      return;
+    }
+
+  if (fndecl)
+    {
+      if (dump_file)
+	{
+	  fprintf (dump_file, "Direct call, calling to ");
+	  print_generic_expr (dump_file, fndecl, TDF_SLIM);
+	  fprintf (dump_file, "\n");
+	}
+      if (dump_enabled_p ())
+	{
+	  dump_printf_loc (MSG_OPTIMIZED_LOCATIONS, call,
+			   "converting virtual pointer-to-member call to %s\n",
+			   lang_hooks.decl_printable_name (fndecl, 2));
+	}
+      gimple_call_set_fndecl (call, fndecl);
+      update_stmt (call);
+      return;
+    }
+
+  auto rebuild_memref = [&info] (gimple *stmt, tree op0, HOST_WIDE_INT op1)
+    {
+      gimple *&slot = info.subst.get_or_insert (stmt);
+      if (slot == NULL)
+	{
+	  gimple_stmt_iterator gsi = gsi_for_stmt (stmt);
+	  op0 = force_gimple_operand_gsi_1 (&gsi, op0, is_gimple_mem_ref_addr,
+					    NULL_TREE, false,
+					    GSI_CONTINUE_LINKING);
+	  tree lhs = make_ssa_name (TREE_TYPE (gimple_get_lhs (stmt)));
+	  tree rhs = gimple_assign_rhs1 (stmt);
+	  rhs = build2 (MEM_REF, TREE_TYPE (rhs), op0,
+			build_int_cst (TREE_TYPE (TREE_OPERAND (rhs, 1)), op1));
+	  gimple *new_stmt = gimple_build_assign (lhs, rhs);
+
+	  gimple_set_vuse (new_stmt, gimple_vuse (stmt));
+	  gsi_insert_after (&gsi, new_stmt, GSI_NEW_STMT);
+	  slot = new_stmt;
+	}
+      return gimple_get_lhs (slot);
+    };
+
+  gcc_checking_assert (idx >= 0);
+
+  tree new_vt = rebuild_memref (SSA_NAME_DEF_STMT (vt),
+				gimple_call_arg (pmf_obj_def, 0), offset);
+  tree new_vfn;
+  /* If target uses descriptors, rebuild a cast instead of MEM_REF.  */
+  if (TARGET_VTABLE_USES_DESCRIPTORS)
+    {
+      gimple *stmt = SSA_NAME_DEF_STMT (vfn);
+      gimple *&slot = info.subst.get_or_insert (stmt);
+      if (slot == NULL)
+	{
+	  gimple_stmt_iterator gsi = gsi_for_stmt (stmt);
+	  tree new_desc = new_vt;
+	  if (byte_idx)
+	    {
+	      new_desc = make_ssa_name (TREE_TYPE (vt));
+	      tree rhs = build2 (POINTER_PLUS_EXPR, TREE_TYPE (new_vt), new_vt,
+				 build_int_cst (sizetype, byte_idx));
+	      gsi_insert_after (&gsi, gimple_build_assign (new_desc, rhs),
+				 GSI_NEW_STMT);
+	    }
+	  new_vfn = make_ssa_name (TREE_TYPE (vfn));
+	  gimple *cast = gimple_build_assign (new_vfn, NOP_EXPR, new_desc);
+	  gsi_insert_after (&gsi, cast, GSI_SAME_STMT);
+	  slot = cast;
+	}
+      new_vfn = gimple_get_lhs (slot);
+    }
+  else
+    new_vfn = rebuild_memref (SSA_NAME_DEF_STMT (vfn), new_vt, byte_idx);
+  if (give_up)
+    {
+      if (dump_file)
+	fprintf (dump_file,
+		 "Giving up: negative offset cannot resolve to a type\n");
+      gimple_call_set_fn (call, new_vfn);
+      update_stmt (call);
+    }
+  else
+    {
+      if (TYPE_BINFO (inner_type)
+	  && polymorphic_type_binfo_p (TYPE_BINFO (inner_type)))
+	{
+	  tree otr_token = build_int_cst (integer_type_node, idx);
+	  tree new_method_type
+	    = build_method_type_directly (inner_type, TREE_TYPE (method_type),
+					  TREE_CHAIN
+					    (TYPE_ARG_TYPES (method_type)));
+	  tree otr = build3 (OBJ_TYPE_REF, build_pointer_type (new_method_type),
+			     new_vfn, gimple_call_arg (call, 0),
+			     fold_convert (build_pointer_type (inner_type),
+					   otr_token));
+	  if (dump_file)
+	    {
+	      fprintf (dump_file, "Recognized virtual call, emitting OTR ");
+	      print_generic_expr (dump_file, otr, TDF_SLIM);
+	      fprintf (dump_file, "\n");
+	    }
+	  gimple_call_set_fn (call, otr);
+	}
+      else
+	{
+	  if (dump_file)
+	    {
+	      fprintf (dump_file, "Giving up: inner type ");
+	      print_generic_expr (dump_file, inner_type, TDF_SLIM);
+	      fprintf (dump_file, " is not polymorphic\n");
+	    }
+	  gimple_call_set_fn (call, new_vfn);
+	}
+      update_stmt (call);
+    }
+  return;
+}
+
+const pass_data pass_data_ptrmemfunc =
+{
+  GIMPLE_PASS, /* type */
+  "ptrmemfunc", /* name */
+  OPTGROUP_NONE, /* optinfo_flags */
+  TV_GIMPLE_PTRMEMFUNC, /* tv_id */
+  ( PROP_cfg | PROP_ssa ), /* properties_required */
+  0, /* properties_provided */
+  0, /* properties_destroyed */
+  0, /* todo_flags_start */
+  0, /* todo_flags_finish */
+};
+
+class pass_ptrmemfunc : public gimple_opt_pass
+{
+public:
+  pass_ptrmemfunc (gcc::context *ctxt)
+    : gimple_opt_pass (pass_data_ptrmemfunc, ctxt)
+  {}
+
+  /* opt_pass methods: */
+  opt_pass * clone () final override { return new pass_ptrmemfunc (m_ctxt); }
+  bool gate (function *fun) final override
+  {
+    /* Skip IL scan if the flag is not set.  It is set by forwprop currently so
+       this pass must run after forwprop to actually do any work.  */
+    return optimize != 0
+	   && flag_devirtualize
+	   && (fun->pending_TODOs & PENDING_TODO_ptrmemfunc_devirt);
+  }
+  unsigned int execute (function *) final override;
+
+}; // class pass_ptrmemfunc
+
+unsigned int
+pass_ptrmemfunc::execute (function *fun)
+{
+  fun->pending_TODOs &= ~PENDING_TODO_ptrmemfunc_devirt;
+
+  basic_block bb;
+  pass_ptrmemfunc_func_info info;
+
+  unsigned int todo_flags = 0;
+  FOR_EACH_BB_FN (bb, fun)
+    {
+      for (gimple_stmt_iterator gsi = gsi_start_bb (bb); !gsi_end_p (gsi);)
+	{
+	  gimple *stmt = gsi_stmt (gsi);
+	  if (!is_gimple_call (stmt))
+	    {
+	      gsi_next (&gsi);
+	      continue;
+	    }
+
+	  fold_virtual_ptrmemfunc_call (as_a<gcall *> (stmt), info);
+	  gsi_next (&gsi);
+	}
+    }
+
+  while (!info.unreachables.is_empty ())
+    {
+      gimple *stmt = info.unreachables.pop ();
+      if (fixup_noreturn_call (stmt))
+	todo_flags |= TODO_cleanup_cfg;
+    }
+
+  bool do_eh_cleanup = !bitmap_empty_p (info.dead_eh);
+  bool do_dce = !bitmap_empty_p (info.dead_ssas);
+
+  if (do_eh_cleanup)
+    {
+      gimple_purge_all_dead_eh_edges (info.dead_eh);
+      todo_flags |= TODO_cleanup_cfg;
+    }
+  if (do_dce)
+    simple_dce_from_worklist (info.dead_ssas);
+
+  return todo_flags;
+}
+
+} // anon namespace
+
+gimple_opt_pass *
+make_pass_ptrmemfunc (gcc::context *ctxt)
+{
+  return new pass_ptrmemfunc (ctxt);
+}
diff --git a/gcc/gimple-ptrmemfunc.h b/gcc/gimple-ptrmemfunc.h
new file mode 100644
index 00000000000..e6bfa256f10
--- /dev/null
+++ b/gcc/gimple-ptrmemfunc.h
@@ -0,0 +1,29 @@
+/* Header file for gimple constant pointer to member function calls.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_GIMPLE_PTRMEMFUNC_H
+#define GCC_GIMPLE_PTRMEMFUNC_H
+
+bool gimple_analyze_virtual_ptrmemfunc_branch (tree vfn, tree &pmf_obj);
+tree find_outermost_base_at_offset (tree type, HOST_WIDE_INT offset);
+tree get_ptrmemfunc_polymorphic_direct_target (tree base, tree inner,
+					       poly_int64 offset,
+					       HOST_WIDE_INT idx);
+
+#endif // GCC_GIMPLE_PTRMEMFUNC_H
diff --git a/gcc/internal-fn.cc b/gcc/internal-fn.cc
index 0138c6f7ef0..80d65094318 100644
--- a/gcc/internal-fn.cc
+++ b/gcc/internal-fn.cc
@@ -456,6 +456,17 @@ expand_ANNOTATE (internal_fn, gcall *)
   gcc_unreachable ();
 }
 
+static void
+expand_PTRMEMFUNC_OBJ (internal_fn, gcall *call)
+{
+  tree lhs = gimple_call_lhs (call);
+
+  if (!lhs)
+    return;
+
+  expand_assignment (lhs, gimple_call_arg (call, 0), false);
+}
+
 /* This should get expanded in omp_device_lower pass.  */
 
 static void
diff --git a/gcc/internal-fn.def b/gcc/internal-fn.def
index af9f92950c7..47db056400f 100644
--- a/gcc/internal-fn.def
+++ b/gcc/internal-fn.def
@@ -649,6 +649,9 @@ DEF_INTERNAL_FN (BITINTTOFLOAT, ECF_PURE | ECF_LEAF, ". R . ")
 DEF_INTERNAL_OPTAB_FN (BIT_ANDN, ECF_CONST, andn, binary)
 DEF_INTERNAL_OPTAB_FN (BIT_IORN, ECF_CONST, iorn, binary)
 
+/* Pointer-to-member calling object wrapper.  See gimple-ptrmemfunc.cc.  */
+DEF_INTERNAL_FN (PTRMEMFUNC_OBJ, ECF_CONST | ECF_LEAF | ECF_NOTHROW, NULL)
+
 #undef DEF_INTERNAL_WIDENING_OPTAB_FN
 #undef DEF_INTERNAL_SIGNED_COND_FN
 #undef DEF_INTERNAL_COND_FN
diff --git a/gcc/passes.def b/gcc/passes.def
index 9095c134f49..78ce50d3282 100644
--- a/gcc/passes.def
+++ b/gcc/passes.def
@@ -97,6 +97,9 @@ along with GCC; see the file COPYING3.  If not see
 	  NEXT_PASS (pass_sccopy);
 	  NEXT_PASS (pass_merge_phi);
           NEXT_PASS (pass_dse);
+	  /* This pass must be after forwprop since it only scans functions with
+	     PENDING_TODO_ptrmemfunc_devirt.  */
+	  NEXT_PASS (pass_ptrmemfunc);
 	  NEXT_PASS (pass_cd_dce, false /* update_address_taken_p */, true /* remove_unused_locals */);
 	  NEXT_PASS (pass_phiopt, true /* early_p */);
 	  /* Cleanup eh is done before tail recusision to remove empty (only clobbers)
@@ -232,6 +235,9 @@ along with GCC; see the file COPYING3.  If not see
       NEXT_PASS (pass_build_alias);
       NEXT_PASS (pass_return_slot);
       NEXT_PASS (pass_fre, true /* may_iterate */);
+      /* This pass must be after forwprop since it only scans functions with
+	 PENDING_TODO_ptrmemfunc_devirt.  */
+      NEXT_PASS (pass_ptrmemfunc);
       NEXT_PASS (pass_merge_phi);
       NEXT_PASS (pass_thread_jumps_full, /*first=*/true);
       NEXT_PASS (pass_vrp, false /* final_p*/);
diff --git a/gcc/testsuite/g++.dg/opt/devirtpmf1.C b/gcc/testsuite/g++.dg/opt/devirtpmf1.C
new file mode 100644
index 00000000000..aad7f43a5fc
--- /dev/null
+++ b/gcc/testsuite/g++.dg/opt/devirtpmf1.C
@@ -0,0 +1,128 @@
+// GIMPLE tree devirtualizable cases
+// { dg-do compile { target c++11 } }
+// { dg-options "-O2 -Wno-inaccessible-base" }
+
+// Plain
+struct SA
+{
+  virtual void f1 ();
+  virtual void f2 ();
+  virtual void f3 ();
+};
+struct SB
+{
+  virtual void g1 ();
+  virtual void g2 ();
+  virtual void g3 ();
+};
+struct S final : SA, SB
+{
+  virtual void h1 ();
+  virtual void h2 ();
+  virtual void h3 ();
+};
+
+void
+func1 (S *obj, S &ref)
+{
+  // { dg-final { scan-assembler-times "_ZN2SA2f1Ev" 2 } }
+  // { dg-final { scan-assembler-times "_ZN2SB2g2Ev" 2 } }
+  // { dg-final { scan-assembler-times "_ZN1S2h3Ev" 2 } }
+  (ref.*&SA::f1) ();
+  (ref.*&SB::g2) ();
+  (ref.*&S::h3) ();
+  (obj->*&SA::f1) ();
+  (obj->*&SB::g2) ();
+  (obj->*&S::h3) ();
+}
+
+// Virtual inheritance
+struct TB
+{
+  virtual void foo ();
+  virtual void bar ();
+};
+struct TL : virtual TB { virtual void foo (); };
+struct TR : virtual TB { virtual void bar (); };
+struct T final : TL, TR {};
+
+void
+func2 (T *obj)
+{
+  // { dg-final { scan-assembler-times "_ZN2TL3fooEv" 1 } }
+  // { dg-final { scan-assembler-times "_ZTv0_n\\d+_N2TR3barEv" 2 } }
+  (obj->*(&TB::foo)) ();
+  (obj->*(&TB::bar)) ();
+  (obj->*(&TL::bar)) ();
+}
+
+// Ambiguous base
+struct U0
+{
+  virtual void foo ();
+};
+struct U1 : U0 {};
+struct U2 : U0, U1
+{
+  virtual void foo () final;
+};
+
+void
+func3 (U2 *obj)
+{
+  // { dg-final { scan-assembler "_ZN2U23fooEv" } }
+  // { dg-final { scan-assembler "_ZThn\\d+_N2U23fooEv" } }
+  (obj->*(&U2::foo)) ();
+  (obj->*static_cast<void (U2::*) ()> (static_cast<void (U1::*) ()> (&U0::foo))) ();
+}
+
+// Duplicated base
+struct VB { virtual void f (); };
+struct VL : VB { void f () final; };
+struct VR : VB { void f () final; };
+struct V : VL, VR {};
+
+void
+func4 (V *obj)
+{
+  // { dg-final { scan-assembler "_ZN2VL1fEv" } }
+  (obj->*static_cast<void (V::*) ()> (&VL::f)) ();
+}
+
+// Covariant return, secondary base
+struct WL { virtual void plain (); };
+struct WR { virtual WR *clone (); };
+struct W : WL, WR { virtual W *clone () final; };
+
+WR*
+func5 (W *obj)
+{
+  // { dg-final { scan-assembler "_ZTchn\\d+_h\\d+_N1W5cloneEv" } }
+  return (obj->*&WR::clone) ();
+}
+
+// Covariant return, primary base
+struct XL { virtual XL *clone (); };
+struct XR { virtual void plain (); };
+struct X : XL, XR { virtual X *clone () final; };
+
+XL*
+func6 (X *obj)
+{
+  // { dg-final { scan-assembler "_ZN1X5cloneEv" } }
+  return (obj->*&XL::clone) ();
+}
+
+// Empty base
+struct YE {};
+struct Y1 { virtual void foo () final; };
+struct Y2 { virtual void bar () final; };
+struct Y : Y1, YE, Y2 {};
+
+void
+func7 (Y *obj)
+{
+  // { dg-final { scan-assembler "_ZN2Y23barEv" } }
+  (obj->*(&Y2::bar)) ();
+}
+
diff --git a/gcc/testsuite/g++.dg/opt/devirtpmf2.C b/gcc/testsuite/g++.dg/opt/devirtpmf2.C
new file mode 100644
index 00000000000..16fead19ef0
--- /dev/null
+++ b/gcc/testsuite/g++.dg/opt/devirtpmf2.C
@@ -0,0 +1,63 @@
+// GIMPLE tree impossible (ill-formed) calls
+// { dg-do compile { target c++11 } }
+// { dg-options "-O3 -fdump-tree-ptrmemfunc" }
+// { dg-final { scan-tree-dump-times "giving __builtin_unreachable \\\(\\\)" 5 "ptrmemfunc1" } }
+// { dg-final { scan-tree-dump "Removing basic block" "ptrmemfunc1" } }
+
+
+struct A { virtual void foo (int); };
+struct B { virtual void bar (int); };
+struct C : A, B
+{
+  virtual void foo (int) final;
+  virtual void bar (int) final;
+};
+struct D : B { virtual void bar2 (int); };
+
+void
+func (C *obj, int arg)
+{
+  (obj
+     ->*(static_cast<void (C::*) (int)> (
+       static_cast<void (B::*) (int)> (&D::bar2)))) (arg);
+}
+
+void
+func2 (C *obj, int arg)
+{
+  (obj
+     ->*(static_cast<void (C::*) (int)> (
+       static_cast<void (B::*) (int)> (&D::bar2)))) (arg);
+  obj->foo (arg);
+}
+
+struct X { virtual void foo (int); };
+struct Y final : X { virtual void foo (int) override; };
+struct Z : X { virtual void bar (int); };
+
+void
+func3 (Y *obj, int arg)
+{
+  (obj->*(static_cast<void (X::*) (int)> (&Z::bar))) (arg);
+}
+
+struct P { virtual void foo (int); };
+
+struct Q : P { virtual ~Q(); };
+
+struct R : P
+{
+  virtual void fake_dtor1 ();
+  virtual void fake_dtor2 ();
+};
+
+void
+func4 (Q *obj)
+{
+  (obj->*(static_cast<void (P::*) ()> (&R::fake_dtor1))) ();
+}
+void
+func5 (Q *obj)
+{
+  (obj->*(static_cast<void (P::*) ()> (&R::fake_dtor2))) ();
+}
diff --git a/gcc/testsuite/g++.dg/opt/devirtpmf3.C b/gcc/testsuite/g++.dg/opt/devirtpmf3.C
new file mode 100644
index 00000000000..2deace7b2c6
--- /dev/null
+++ b/gcc/testsuite/g++.dg/opt/devirtpmf3.C
@@ -0,0 +1,57 @@
+// GIMPLE tree not devirtualizable tests
+// { dg-do compile { target c++11 } }
+// { dg-options "-O3 -fdump-tree-ptrmemfunc" }
+// { dg-final { scan-tree-dump-not "giving __builtin_unreachable \\\(\\\)" "ptrmemfunc1" } }
+// { dg-final { scan-tree-dump-not "Direct call, calling to" "ptrmemfunc1" } }
+// { dg-final { scan-tree-dump-not "\\.PTRMEMFUNC_OBJ" "ptrmemfunc1" } }
+// { dg-final { scan-tree-dump "Giving up: inner type.*X.*is not polymorphic" "ptrmemfunc1" } }
+
+
+// Not final
+struct S { virtual void foo (); };
+
+void
+func1 (S *obj)
+{
+  (obj->*(&S::foo)) ();
+}
+
+// Not final, from primary derived
+struct T1 { virtual void foo (); };
+struct T2 : T1 { virtual void bar (); };
+
+void
+func2 ()
+{
+  T2 obj;
+  T1 &base = obj;
+  (base.*(static_cast<void (T1::*) ()> (&T2::bar))) ();
+}
+
+void
+func3 (T1 *obj)
+{
+  (obj->*(static_cast<void (T1::*) ()> (&T2::bar))) ();
+}
+
+// Negative offset
+struct A { virtual void foo (); };
+struct B { virtual void bar (); };
+struct C : A, B {};
+
+void
+func4 (B *b)
+{
+  (b->*(static_cast<void (B::*) ()>
+          (static_cast<void (C::*) ()> (&A::foo)))) ();
+}
+
+// Non-polymorphic base
+struct X {};
+struct Y : X { virtual void bar () final; };
+
+void
+func5 (X *x)
+{
+  (x->*(static_cast<void (X::*) ()> (&Y::bar))) ();
+}
diff --git a/gcc/testsuite/g++.dg/opt/devirtpmf4.C b/gcc/testsuite/g++.dg/opt/devirtpmf4.C
new file mode 100644
index 00000000000..4dbe5c799b3
--- /dev/null
+++ b/gcc/testsuite/g++.dg/opt/devirtpmf4.C
@@ -0,0 +1,45 @@
+// GIMPLE tree devirtualizable tests, with offset adjusting operations
+// { dg-do compile { target c++11 } }
+// { dg-options "-O2" }
+// { dg-final { scan-assembler-times "_ZN1B6abcabcEv" 8 } }
+
+
+struct A { virtual void abcabc (); };
+struct B : A { void abcabc () final; };
+
+void
+func1 (B *arr[16])
+{
+  (arr[6]->*&B::abcabc) ();
+}
+
+void
+func2 (B *arr[16])
+{
+  (*(arr + 7)->*&B::abcabc) ();
+}
+
+struct C
+{
+  int a[16];
+  B b;
+  int c[32];
+  B d[2];
+  B* e;
+};
+
+void
+func3 (C *c)
+{
+  (&(c->b)->*&B::abcabc) ();
+  ((c->d + 1)->*&B::abcabc) ();
+  ((c->e)->*&B::abcabc) ();
+}
+
+void
+func4 (C c[16])
+{
+  (&((c + 6)->b)->*&A::abcabc) ();
+  (((c + 7)->d + 1)->*&A::abcabc) ();
+  (((c + 8)->e)->*&A::abcabc) ();
+}
diff --git a/gcc/testsuite/g++.dg/opt/devirtpmf5.C b/gcc/testsuite/g++.dg/opt/devirtpmf5.C
new file mode 100644
index 00000000000..afacc3b90f5
--- /dev/null
+++ b/gcc/testsuite/g++.dg/opt/devirtpmf5.C
@@ -0,0 +1,72 @@
+// GIMPLE tree devirtualizable tests, with template expansions
+// { dg-do compile { target c++11 } }
+// { dg-options "-O2" }
+
+
+template<typename PMF, PMF mem, typename Cast = PMF, typename T>
+[[gnu::always_inline]] inline void
+dispatch (T *obj) { (obj->*static_cast<Cast> (mem)) (); }
+
+// Plain
+struct SA
+{
+  virtual void f1 ();
+  virtual void f2 ();
+  virtual void f3 ();
+};
+struct SB
+{
+  virtual void g1 ();
+  virtual void g2 ();
+  virtual void g3 ();
+};
+struct S final : SA, SB
+{
+  virtual void h1 ();
+  virtual void h2 ();
+  virtual void h3 ();
+};
+
+void
+func1 (S *obj)
+{
+  // { dg-final { scan-assembler "_ZN2SA2f1Ev" } }
+  // { dg-final { scan-assembler "_ZN2SB2g2Ev" } }
+  // { dg-final { scan-assembler "_ZN1S2h3Ev" } }
+  dispatch <decltype (&SA::f1), &SA::f1> (obj);
+  dispatch <decltype (&SB::g2), &SB::g2> (obj);
+  dispatch <decltype (&S::h3), &S::h3> (obj);
+}
+
+struct TB
+{
+  virtual void foo ();
+  virtual void bar ();
+};
+struct TL : virtual TB { virtual void foo (); };
+struct TR : virtual TB { virtual void bar (); };
+struct T final : TL, TR {};
+
+void
+func2 (T *obj)
+{
+  // { dg-final { scan-assembler-times "_ZN2TL3fooEv" 1 } }
+  // { dg-final { scan-assembler-times "_ZTv0_n\\d+_N2TR3barEv" 2 } }
+  dispatch <decltype (&TB::foo), &TB::foo> (obj);
+  dispatch <decltype (&TB::bar), &TB::bar> (obj);
+  dispatch <decltype (&TL::bar), &TL::bar> (obj);
+}
+
+// Duplicated base
+struct UB { virtual void f (); };
+struct UL : UB { void f () final; };
+struct UR : UB { void f () final; };
+struct U : UL, UR {};
+
+void
+func4 (U *obj)
+{
+  // { dg-final { scan-assembler "_ZN2UL1fEv" } }
+  dispatch<decltype (&UL::f), &UL::f> (obj);
+  dispatch<decltype (&UL::f), &UL::f, void (U::*) ()> (obj);
+}
diff --git a/gcc/timevar.def b/gcc/timevar.def
index fc78600b652..ea20ffebde6 100644
--- a/gcc/timevar.def
+++ b/gcc/timevar.def
@@ -324,6 +324,7 @@ DEFTIMEVAR (TV_WARN_ACCESS           , "access analysis")
 DEFTIMEVAR (TV_GIMPLE_CRC_OPTIMIZATION, "crc optimization")
 DEFTIMEVAR (TV_EXT_DCE               , "ext dce")
 DEFTIMEVAR (TV_FOLD_MEM_OFFSETS      , "fold mem offsets")
+DEFTIMEVAR (TV_GIMPLE_PTRMEMFUNC     , "polymorphic pmf devirt")
 
 /* Everything else in rest_of_compilation not included above.  */
 DEFTIMEVAR (TV_EARLY_LOCAL	     , "early local passes")
diff --git a/gcc/tree-pass.h b/gcc/tree-pass.h
index a3b35e009e0..4d0b6c42a71 100644
--- a/gcc/tree-pass.h
+++ b/gcc/tree-pass.h
@@ -322,6 +322,8 @@ protected:
 /* Tell the next scalar cleanup pass that there is
    work for it to do.  */
 #define PENDING_TODO_force_next_scalar_cleanup  (1 << 1)
+/* Tell the gimple-ptrmemfunc pass that there is work for it to do.  */
+#define PENDING_TODO_ptrmemfunc_devirt		(1 << 2)
 
 /* Register pass info. */
 
@@ -510,6 +512,7 @@ extern gimple_opt_pass *make_pass_modref (gcc::context *ctxt);
 extern gimple_opt_pass *make_pass_coroutine_lower_builtins (gcc::context *ctxt);
 extern gimple_opt_pass *make_pass_coroutine_early_expand_ifns (gcc::context *ctxt);
 extern gimple_opt_pass *make_pass_adjust_alignment (gcc::context *ctxt);
+extern gimple_opt_pass *make_pass_ptrmemfunc (gcc::context *ctxt);
 
 /* IPA Passes */
 extern simple_ipa_opt_pass *make_pass_ipa_lower_emutls (gcc::context *ctxt);
diff --git a/gcc/tree-ssa-forwprop.cc b/gcc/tree-ssa-forwprop.cc
index 75f06c6ba41..57b5350ba23 100644
--- a/gcc/tree-ssa-forwprop.cc
+++ b/gcc/tree-ssa-forwprop.cc
@@ -5919,6 +5919,10 @@ pass_forwprop::execute (function *fun)
 		    if (callee != NULL_TREE
 			&& fndecl_built_in_p (callee, BUILT_IN_NORMAL))
 		      changed |= simplify_builtin_call (&gsi, callee, full_walk);
+		    /* Tag a pending_TODO to avoid walking unnecessary functions
+		       in gimple-ptrmemfunc.  */
+		    if (gimple_call_internal_p (stmt, IFN_PTRMEMFUNC_OBJ))
+		      fun->pending_TODOs |= PENDING_TODO_ptrmemfunc_devirt;
 		    break;
 		  }
 
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.