> On 9 Sep 2026, at 15:04, Richard Biener <[email protected]> wrote:
>
> On Fri, 4 Sep 2026, [email protected] wrote:
>
>> From: Kyrylo Tkachov <[email protected]>
>>
>> A REFERENCE_TYPE parameter that refers to an object designates an object that
>> is at least as large as its referred-to type. A method's this parameter
>> designates an object that is at least as large as TYPE_METHOD_BASETYPE.
>> tree_could_trap_p already uses this property for the this parameter, but not
>> for other reference parameters.
>>
>> LIM therefore cannot move safe invariant loads through reference parameters.
>>
>> GAPBS bfs contains this case in BUStep. Without this change, the loads
>> remain below conditional guards in loop 1:
>>
>> <bb 6> _30 = MEM[(const struct CSRGraph *)g_15(D)].in_index_;
>> <bb 13> _38 = MEM[(const struct Bitmap *)front_22(D)].start_;
>>
>> Factor the parameter object-size query into whole_object_param_size.
>> Handle reference parameters and this parameters there. Use nonnull_arg_p
>> as the policy gate, in line with -fdelete-null-pointer-checks. LIM can
>> then move both loads to the loop-1 preheader:
>>
>> <bb 12> _30 = MEM[(const struct CSRGraph *)g_15(D)].in_index_;
>> _38 = MEM[(const struct Bitmap *)front_22(D)].start_;
>>
>> Use the object bound only for rvalues. A reference or this parameter can
>> designate read-only storage after const_cast, so its object bound does not
>> prove that a store is safe.
>>
>> Add tests for load motion, access bounds, the null-pointer policy,
>> conditional store elimination, store motion, built-in memory attributes, and
>> strub watermark attributes. Cover reference parameters and this parameters.
>>
>> On Grace, serial GAPBS bfs with kron-23, -O3, and -mcpu=grace
>> reduces cycles by 6.2%.
>>
>> Bootstrapped and tested on aarch64-unknown-linux-gnu.
>> Ok for trunk?
>
> OK. Let's be prepared to roll back in case too much code in the
> wild gets this wrong (you never know).
>
Thanks. Maybe we can have a fine-grained option like we do for
-fdelete-null-pointer-checks to allow code to opt in/out? I guess we can easily
add one later in the cycle if any breakage appears.
Kyrill
> Thanks,
> Richard.
>
>> Thanks,
>> Kyrill
>>
>> gcc/ChangeLog:
>>
>> * doc/generic.texi (REFERENCE_TYPE): Document the object contract
>> for reference parameters.
>> (METHOD_TYPE): Document the object contract for the this parameter.
>> * tree-eh.cc (parm_decl_from_ptr): New function.
>> (whole_object_param_size): New function.
>> (tree_could_trap_1): Use whole-object parameter bounds for loads.
>>
>> gcc/testsuite/ChangeLog:
>>
>> * c-c++-common/strub-mem-notrap.c: New test.
>> * g++.dg/tree-ssa/cselim-ref-1.C: New test.
>> * g++.dg/tree-ssa/cselim-ref-2.C: New test.
>> * g++.dg/tree-ssa/lim-ref-1.C: New test.
>> * g++.dg/tree-ssa/lim-ref-2.C: New test.
>> * g++.dg/tree-ssa/lim-ref-3.C: New test.
>> * g++.dg/tree-ssa/lim-ref-4.C: New test.
>> * g++.dg/tree-ssa/lim-this-1.C: New test.
>> * g++.dg/tree-ssa/noncall-store-builtin.C: New test.
>>
>> Signed-off-by: Kyrylo Tkachov <[email protected]>
>> ---
>> gcc/doc/generic.texi | 7 +-
>> gcc/testsuite/c-c++-common/strub-mem-notrap.c | 21 ++++++
>> gcc/testsuite/g++.dg/tree-ssa/cselim-ref-1.C | 29 ++++++++
>> gcc/testsuite/g++.dg/tree-ssa/cselim-ref-2.C | 26 +++++++
>> gcc/testsuite/g++.dg/tree-ssa/lim-ref-1.C | 21 ++++++
>> gcc/testsuite/g++.dg/tree-ssa/lim-ref-2.C | 21 ++++++
>> gcc/testsuite/g++.dg/tree-ssa/lim-ref-3.C | 22 ++++++
>> gcc/testsuite/g++.dg/tree-ssa/lim-ref-4.C | 30 ++++++++
>> gcc/testsuite/g++.dg/tree-ssa/lim-this-1.C | 25 +++++++
>> .../g++.dg/tree-ssa/noncall-store-builtin.C | 20 +++++
>> gcc/tree-eh.cc | 73 +++++++++++++++----
>> 11 files changed, 278 insertions(+), 17 deletions(-)
>> create mode 100644 gcc/testsuite/c-c++-common/strub-mem-notrap.c
>> create mode 100644 gcc/testsuite/g++.dg/tree-ssa/cselim-ref-1.C
>> create mode 100644 gcc/testsuite/g++.dg/tree-ssa/cselim-ref-2.C
>> create mode 100644 gcc/testsuite/g++.dg/tree-ssa/lim-ref-1.C
>> create mode 100644 gcc/testsuite/g++.dg/tree-ssa/lim-ref-2.C
>> create mode 100644 gcc/testsuite/g++.dg/tree-ssa/lim-ref-3.C
>> create mode 100644 gcc/testsuite/g++.dg/tree-ssa/lim-ref-4.C
>> create mode 100644 gcc/testsuite/g++.dg/tree-ssa/lim-this-1.C
>> create mode 100644 gcc/testsuite/g++.dg/tree-ssa/noncall-store-builtin.C
>>
>> diff --git a/gcc/doc/generic.texi b/gcc/doc/generic.texi
>> index 42a04e34eda..e61b151cd86 100644
>> --- a/gcc/doc/generic.texi
>> +++ b/gcc/doc/generic.texi
>> @@ -512,7 +512,10 @@ Used to represent pointer types, and pointer to data
>> member types. The
>>
>> @item REFERENCE_TYPE
>> Used to represent reference types. The @code{TREE_TYPE} gives the type
>> -to which this type refers.
>> +to which this type refers. A @code{PARM_DECL} whose reference type refers
>> +to an object type must designate an object whose size is at least that of
>> +the referred-to type. Front ends must use @code{POINTER_TYPE} for a
>> +parameter that can be absent or otherwise fail to designate such an object.
>>
>> @item FUNCTION_TYPE
>> Used to represent the type of non-member functions and of static member
>> @@ -537,6 +540,8 @@ The type of @code{*this}, i.e., the class of which
>> functions of this
>> type are a member, is given by the @code{TYPE_METHOD_BASETYPE}. The
>> @code{TYPE_ARG_TYPES} is the parameter list, as for a
>> @code{FUNCTION_TYPE}, and includes the @code{this} argument.
>> +The @code{this} argument must designate an object whose size is at least
>> +that of @code{TYPE_METHOD_BASETYPE}.
>>
>> @item ARRAY_TYPE
>> Used to represent array types. The @code{TREE_TYPE} gives the type of
>> diff --git a/gcc/testsuite/c-c++-common/strub-mem-notrap.c
>> b/gcc/testsuite/c-c++-common/strub-mem-notrap.c
>> new file mode 100644
>> index 00000000000..ef9bbc39535
>> --- /dev/null
>> +++ b/gcc/testsuite/c-c++-common/strub-mem-notrap.c
>> @@ -0,0 +1,21 @@
>> +/* Keep read and write attributes separate for a strub watermark update. */
>> +
>> +/* { dg-do compile } */
>> +/* { dg-options "-O2 -fstrub=strict -fnon-call-exceptions" } */
>> +/* { dg-additional-options "-fdelete-null-pointer-checks -fdump-rtl-expand"
>> } */
>> +/* { dg-require-effective-target strub } */
>> +/* { dg-skip-if "" keeps_null_pointer_checks } */
>> +
>> +volatile int sink;
>> +
>> +__attribute__ ((__strub__ ("internal"), noinline))
>> +int
>> +scrubbed (int value)
>> +{
>> + sink = value;
>> + return sink;
>> +}
>> +
>> +/* The watermark load is nontrapping. The conditional store can trap. */
>> +/* { dg-final { scan-rtl-dump {\(mem/f/c:[^\n]*watermark_ptr} "expand" } }
>> */
>> +/* { dg-final { scan-rtl-dump {\(mem/f:[^\n]*watermark_ptr} "expand" } } */
>> diff --git a/gcc/testsuite/g++.dg/tree-ssa/cselim-ref-1.C
>> b/gcc/testsuite/g++.dg/tree-ssa/cselim-ref-1.C
>> new file mode 100644
>> index 00000000000..ede771ecfc0
>> --- /dev/null
>> +++ b/gcc/testsuite/g++.dg/tree-ssa/cselim-ref-1.C
>> @@ -0,0 +1,29 @@
>> +// A reference parameter proves that an in-bounds load is safe. It does not
>> +// prove that the referenced object is writable. Keep the store
>> conditional.
>> +
>> +// { dg-do compile }
>> +// { dg-options "-O2 -fdelete-null-pointer-checks -ftree-cselim
>> -fdump-tree-phiopt1-details" }
>> +// { dg-additional-options "-fallow-store-data-races" }
>> +// { dg-skip-if "" keeps_null_pointer_checks }
>> +
>> +void
>> +f (const int &a, int b, int d)
>> +{
>> + int &c = const_cast<int &> (a);
>> + int p = a;
>> + if (b)
>> + c = d | p;
>> +}
>> +
>> +void
>> +g (int &a, int b, int d)
>> +{
>> + // A non-const reference can designate a const object after const_cast.
>> + // The path that does not store is still valid.
>> + int p = a;
>> + if (b)
>> + a = d | p;
>> +}
>> +
>> +// { dg-final { scan-tree-dump-times "if \\(b_" 2 "phiopt1" } }
>> +// { dg-final { scan-tree-dump-not "Conditional store replacement happened"
>> "phiopt1" } }
>> diff --git a/gcc/testsuite/g++.dg/tree-ssa/cselim-ref-2.C
>> b/gcc/testsuite/g++.dg/tree-ssa/cselim-ref-2.C
>> new file mode 100644
>> index 00000000000..16aeceb6654
>> --- /dev/null
>> +++ b/gcc/testsuite/g++.dg/tree-ssa/cselim-ref-2.C
>> @@ -0,0 +1,26 @@
>> +// Check the full conditional store replacement after disabling the early
>> +// phi optimization.
>> +
>> +// { dg-do compile }
>> +// { dg-options "-O2 -fdelete-null-pointer-checks -ftree-cselim
>> -fno-ssa-phiopt -fdump-tree-cselim-details" }
>> +// { dg-additional-options "-fallow-store-data-races" }
>> +// { dg-skip-if "" keeps_null_pointer_checks }
>> +
>> +void
>> +f (const int &a, int b, int d)
>> +{
>> + if (b)
>> + const_cast<int &> (a) = d;
>> +}
>> +
>> +void
>> +g (int &a, int b, int d)
>> +{
>> + // A non-const reference can designate a const object after const_cast.
>> + // The path that does not store is still valid.
>> + if (b)
>> + a = d;
>> +}
>> +
>> +// { dg-final { scan-tree-dump-times "if \\(b_" 2 "cselim" } }
>> +// { dg-final { scan-tree-dump-not "Conditional store replacement happened"
>> "cselim" } }
>> diff --git a/gcc/testsuite/g++.dg/tree-ssa/lim-ref-1.C
>> b/gcc/testsuite/g++.dg/tree-ssa/lim-ref-1.C
>> new file mode 100644
>> index 00000000000..fcb9a7cbd7b
>> --- /dev/null
>> +++ b/gcc/testsuite/g++.dg/tree-ssa/lim-ref-1.C
>> @@ -0,0 +1,21 @@
>> +// A parameter of reference type is bound to a valid object of the
>> referenced
>> +// type, so a load from it cannot trap and may be hoisted out of the loop
>> even
>> +// though it is only executed on some iterations.
>> +
>> +// { dg-do compile }
>> +// { dg-options "-O2 -fdelete-null-pointer-checks -fdump-tree-lim2-details"
>> }
>> +// { dg-skip-if "" keeps_null_pointer_checks }
>> +
>> +struct S { int n; int *p; };
>> +
>> +int
>> +f (S &s, int n)
>> +{
>> + int r = 0;
>> + for (int i = 0; i < n; ++i)
>> + if (i & 1)
>> + r += s.p[i];
>> + return r;
>> +}
>> +
>> +// { dg-final { scan-tree-dump "Moving statement \[^\n\r]*->p;" "lim2" } }
>> diff --git a/gcc/testsuite/g++.dg/tree-ssa/lim-ref-2.C
>> b/gcc/testsuite/g++.dg/tree-ssa/lim-ref-2.C
>> new file mode 100644
>> index 00000000000..e06cb512c1b
>> --- /dev/null
>> +++ b/gcc/testsuite/g++.dg/tree-ssa/lim-ref-2.C
>> @@ -0,0 +1,21 @@
>> +// -fno-delete-null-pointer-checks prevents nonnull_arg_p from using the
>> +// implicit nonnull property of a C++ reference, so the conditional load
>> must
>> +// stay in the loop.
>> +
>> +// { dg-do compile }
>> +// { dg-options "-O2 -fno-delete-null-pointer-checks
>> -fdump-tree-lim2-details" }
>> +
>> +struct S { int n; int *p; };
>> +
>> +int
>> +f (S &s, int n)
>> +{
>> + int r = 0;
>> + for (int i = 0; i < n; ++i)
>> + if (i & 1)
>> + r += s.p[i];
>> + return r;
>> +}
>> +
>> +// { dg-final { scan-tree-dump "Memory reference \[^\n\r]*->p" "lim2" } }
>> +// { dg-final { scan-tree-dump-not "Moving statement \[^\n\r]*->p;" "lim2"
>> } }
>> diff --git a/gcc/testsuite/g++.dg/tree-ssa/lim-ref-3.C
>> b/gcc/testsuite/g++.dg/tree-ssa/lim-ref-3.C
>> new file mode 100644
>> index 00000000000..f4a611d9c47
>> --- /dev/null
>> +++ b/gcc/testsuite/g++.dg/tree-ssa/lim-ref-3.C
>> @@ -0,0 +1,22 @@
>> +// The referenced type bounds what may be speculated: an access past the end
>> +// of the referenced object still counts as trapping and stays in the loop.
>> +
>> +// { dg-do compile }
>> +// { dg-options "-O2 -fdelete-null-pointer-checks -fdump-tree-lim2-details"
>> }
>> +// { dg-skip-if "" keeps_null_pointer_checks }
>> +
>> +struct Small { int a; };
>> +struct Big { int a; long b[8]; };
>> +
>> +long
>> +g (Small &s, int n)
>> +{
>> + long r = 0;
>> + for (int i = 0; i < n; ++i)
>> + if (i & 1)
>> + r += reinterpret_cast<Big &> (s).b[3];
>> + return r;
>> +}
>> +
>> +// { dg-final { scan-tree-dump "Memory reference \[^\n\r]*struct Big"
>> "lim2" } }
>> +// { dg-final { scan-tree-dump-not "Moving statement \[^\n\r]*struct Big"
>> "lim2" } }
>> diff --git a/gcc/testsuite/g++.dg/tree-ssa/lim-ref-4.C
>> b/gcc/testsuite/g++.dg/tree-ssa/lim-ref-4.C
>> new file mode 100644
>> index 00000000000..9a9ac0b845a
>> --- /dev/null
>> +++ b/gcc/testsuite/g++.dg/tree-ssa/lim-ref-4.C
>> @@ -0,0 +1,30 @@
>> +// The object bound of a reference parameter does not make a conditional
>> +// store safe to execute outside its guard.
>> +
>> +// { dg-do compile }
>> +// { dg-options "-O2 -fdelete-null-pointer-checks -fno-tree-cselim
>> -fno-ssa-phiopt -fdump-tree-lim2-details" }
>> +// { dg-additional-options "-fallow-store-data-races" }
>> +// { dg-skip-if "" keeps_null_pointer_checks }
>> +
>> +struct S { int value; };
>> +
>> +void
>> +f (const S &s, int n, int store)
>> +{
>> + for (int i = 0; i < n; ++i)
>> + if (store)
>> + const_cast<S &> (s).value = 1;
>> +}
>> +
>> +void
>> +g (S &s, int n, int store)
>> +{
>> + // A non-const reference can designate a const object after const_cast.
>> + // The path that does not store is still valid.
>> + for (int i = 0; i < n; ++i)
>> + if (store)
>> + s.value = 1;
>> +}
>> +
>> +// { dg-final { scan-tree-dump-times "Memory reference \[^\n\r]*value" 2
>> "lim2" } }
>> +// { dg-final { scan-tree-dump-not "Executing store motion of
>> \[^\n\r]*value" "lim2" } }
>> diff --git a/gcc/testsuite/g++.dg/tree-ssa/lim-this-1.C
>> b/gcc/testsuite/g++.dg/tree-ssa/lim-this-1.C
>> new file mode 100644
>> index 00000000000..dbfd79a96b5
>> --- /dev/null
>> +++ b/gcc/testsuite/g++.dg/tree-ssa/lim-this-1.C
>> @@ -0,0 +1,25 @@
>> +// -fno-delete-null-pointer-checks prevents nonnull_arg_p from using the
>> +// implicit nonnull property of a method's this parameter, so the
>> conditional
>> +// load must stay in the loop.
>> +
>> +// { dg-do compile }
>> +// { dg-options "-O2 -fno-delete-null-pointer-checks
>> -fdump-tree-lim2-details" }
>> +
>> +struct S
>> +{
>> + int *values;
>> + int f (int);
>> +};
>> +
>> +int
>> +S::f (int n)
>> +{
>> + int r = 0;
>> + for (int i = 0; i < n; ++i)
>> + if (i & 1)
>> + r += values[i];
>> + return r;
>> +}
>> +
>> +// { dg-final { scan-tree-dump "Memory reference \[^\n\r]*->values" "lim2"
>> } }
>> +// { dg-final { scan-tree-dump-not "Moving statement \[^\n\r]*->values;"
>> "lim2" } }
>> diff --git a/gcc/testsuite/g++.dg/tree-ssa/noncall-store-builtin.C
>> b/gcc/testsuite/g++.dg/tree-ssa/noncall-store-builtin.C
>> new file mode 100644
>> index 00000000000..c99b3d7b3a0
>> --- /dev/null
>> +++ b/gcc/testsuite/g++.dg/tree-ssa/noncall-store-builtin.C
>> @@ -0,0 +1,20 @@
>> +// Keep the source load of an inline memcpy nontrapping, but not its
>> +// destination store.
>> +
>> +// { dg-do compile }
>> +// { dg-options "-O2 -fdelete-null-pointer-checks -fnon-call-exceptions
>> -finline-stringops=memcpy -fdump-rtl-expand" }
>> +// { dg-require-effective-target ptr32plus }
>> +// { dg-skip-if "" keeps_null_pointer_checks }
>> +
>> +typedef unsigned char block[16384];
>> +
>> +void
>> +copy (block &dest, const block &src)
>> +{
>> + __builtin_memcpy (dest, src, sizeof (block));
>> +}
>> +
>> +// { dg-final { scan-rtl-dump {\(mem(?:/[a-z])*/c(?:/[a-z])*:} "expand" } }
>> +// { dg-final { scan-rtl-dump {\(set[ \t\n]+\(mem(?:/[a-z])*:} "expand" } }
>> +// { dg-final { scan-rtl-dump-not {\(set[
>> \t\n]+\(mem(?:/[a-z])*/c(?:/[a-z])*:} "expand" } }
>> +// { dg-final { scan-assembler-not {\mmemcpy\M} } }
>> diff --git a/gcc/tree-eh.cc b/gcc/tree-eh.cc
>> index e48d9502fb0..e3b8db0b6d0 100644
>> --- a/gcc/tree-eh.cc
>> +++ b/gcc/tree-eh.cc
>> @@ -2752,6 +2752,54 @@ ref_outside_object_p (tree size, poly_offset_int off,
>> tree refsz)
>> return false;
>> }
>>
>> +/* If PTR is a PARM_DECL or its default SSA definition, return the
>> + PARM_DECL. Otherwise return NULL_TREE. */
>> +
>> +static tree
>> +parm_decl_from_ptr (tree ptr)
>> +{
>> + if (TREE_CODE (ptr) == SSA_NAME)
>> + {
>> + if (!SSA_NAME_IS_DEFAULT_DEF (ptr))
>> + return NULL_TREE;
>> + ptr = SSA_NAME_VAR (ptr);
>> + }
>> +
>> + return ptr && TREE_CODE (ptr) == PARM_DECL ? ptr : NULL_TREE;
>> +}
>> +
>> +/* If PTR is a parameter of the current function, or the default definition
>> + of one, that is known to designate a whole object, return the size of
>> that
>> + object in bytes. Otherwise return NULL_TREE.
>> +
>> + Two kinds of parameter qualify. The this pointer of a method points to
>> + an object of the method base type. A parameter whose reference type
>> + refers to an object type is bound to an object of the referenced type.
>> + In both cases the pointed-to object is at least as large as that type.
>> */
>> +
>> +static tree
>> +whole_object_param_size (tree ptr)
>> +{
>> + tree type;
>> +
>> + if (!cfun)
>> + return NULL_TREE;
>> +
>> + ptr = parm_decl_from_ptr (ptr);
>> + if (!ptr)
>> + return NULL_TREE;
>> +
>> + if (TREE_CODE (TREE_TYPE (ptr)) == REFERENCE_TYPE)
>> + type = TREE_TYPE (TREE_TYPE (ptr));
>> + else if (TREE_CODE (TREE_TYPE (cfun->decl)) == METHOD_TYPE
>> + && ptr == DECL_ARGUMENTS (cfun->decl))
>> + type = TYPE_METHOD_BASETYPE (TREE_TYPE (cfun->decl));
>> + else
>> + return NULL_TREE;
>> +
>> + return nonnull_arg_p (ptr) ? TYPE_SIZE_UNIT (type) : NULL_TREE;
>> +}
>> +
>> /* Return true if EXPR can trap, as in dereferencing an invalid pointer
>> location or evaluating floating-point arithmetic. See may_trap_p for the
>> RTL counterpart. This routine expects only GIMPLE lhs or rhs input. LHS
>> @@ -2852,23 +2900,16 @@ tree_could_trap_1 (tree expr, bool lhs)
>> tree refsz = TYPE_SIZE_UNIT (TREE_TYPE (expr));
>> return ref_outside_object_p (size, off, refsz);
>> }
>> - /* See if the base is this for C++ methods. For LHS, this can still
>> - trap. */
>> - if (!lhs && cfun
>> - && TREE_CODE (TREE_TYPE (cfun->decl)) == METHOD_TYPE
>> - && ((TREE_CODE (TREE_OPERAND (expr, 0)) == SSA_NAME
>> - && SSA_NAME_IS_DEFAULT_DEF (TREE_OPERAND (expr, 0))
>> - && (SSA_NAME_VAR (TREE_OPERAND (expr, 0))
>> - == DECL_ARGUMENTS (cfun->decl)))
>> - || TREE_OPERAND (expr, 0) == DECL_ARGUMENTS (cfun->decl)))
>> + if (!lhs)
>> {
>> - poly_offset_int off = mem_ref_offset (expr);
>> - if (maybe_lt (off, 0))
>> - return true;
>> - tree size = TYPE_SIZE_UNIT
>> - (TYPE_METHOD_BASETYPE (TREE_TYPE (cfun->decl)));
>> - tree refsz = TYPE_SIZE_UNIT (TREE_TYPE (expr));
>> - return ref_outside_object_p (size, off, refsz);
>> + if (tree size = whole_object_param_size (TREE_OPERAND (expr, 0)))
>> + {
>> + poly_offset_int off = mem_ref_offset (expr);
>> + if (maybe_lt (off, 0))
>> + return true;
>> + tree refsz = TYPE_SIZE_UNIT (TREE_TYPE (expr));
>> + return ref_outside_object_p (size, off, refsz);
>> + }
>> }
>> return true;
>>
>>
>
> --
> Richard Biener <[email protected]>
> SUSE Software Solutions Germany GmbH,
> Frankenstrasse 146, 90461 Nuernberg, Germany;
> GF: Jochen Jaser, Andrew McDonald, Abhinav Puri; (HRB 36809, AG Nuernberg)