Hi! The following patch adds a new tristate option for optimizations related to replaceable global operators new/delete. The option isn't called -fassume-sane-operator-new (which clang++ implements), because 1) clang++ option means something different; initially it was an option to add malloc attribute to those declarations (but we have malloc attribute on all <new> calls already unconditionally); later it was changed to add noalias attribute rather than malloc, whatever it means, but it is certainly about the return value from the operator new (whether it can alias with other pointers); we already assume malloc-ish behavior that it doesn't alias any other pointers 2) the option only affects operator new, we want it affect also operator delete In the past, before PR101480 fix in 2021, we treated the replaceable global operators new/delete when called from new/delete expressions similarly to malloc/free, i.e. we assumed beyond their described behavior they act like const functions, don't read or modify global state (observable in current TU). That is fine when we treat malloc/free as a blackbox and hope people don't use malloc hooks or don't do something weird in them, or when the replaceable global operators use new/delete under the hood and don't do anything else (except perhaps throwing exceptions) beyond that. clang++ behavior (again only for replaceable global operators new/delete when called from new/delete expressions (or __builtin_operator_{new,delete}) is apparently that they act like pure functions beyond what they are documented to do, i.e. can read global state (aka g++.dg/torture/pr101480.C testcase is considered valid), but can't modify it. I think that is because of the license from https://eel.is/c++draft/expr.new#14 https://eel.is/c++draft/expr.delete#6 that those calls could be omitted, I think valid C++ programs just can't rely on some observable global state modification done by those operators happening.
The patch below makes the clang++ behavior the default (i.e. pure-ish operator new/delete when called from new/delete expressions, no assumptions when called directly) and adds options to override this behavior in both directions, to the pre-PR101480 state with e.g. -fassume-sane-operators-new-delete (in fact a little bit further; because the patch makes those assumptions even when calling the operators directly), and to the current trunk state with e.g. -fno-assume-sane-operators-new-delete. I've tried to explain stuff in the documentation too. So far smoke tested, ok for trunk if it passes full bootstrap/regtest? 2024-11-15 Jakub Jelinek <ja...@redhat.com> PR c++/110137 gcc/ * doc/invoke.texi (-fassume-sane-operators-new-delete, -fno-assume-sane-operators-new-delete, -fassume-sane-operators-new-delete=): Document. * gimple.cc (gimple_call_fnspec): Handle -f{,no-}assume-sane-operators-new-delete{,={0,1,2}}. gcc/c-family/ * c.opt (fassume-sane-operators-new-delete, fassume-sane-operators-new-delete=): New options. gcc/testsuite/ * g++.dg/tree-ssa/pr110137-1.C: New test. * g++.dg/tree-ssa/pr110137-2.C: New test. * g++.dg/tree-ssa/pr110137-3.C: New test. * g++.dg/tree-ssa/pr110137-4.C: New test. * g++.dg/tree-ssa/pr110137-5.C: New test. * g++.dg/tree-ssa/pr110137-6.C: New test. --- gcc/c-family/c.opt.jj 2024-11-15 13:13:29.021751829 +0100 +++ gcc/c-family/c.opt 2024-11-15 15:35:02.401561741 +0100 @@ -1686,6 +1686,14 @@ fasm C ObjC C++ ObjC++ Var(flag_no_asm, 0) Recognize the \"asm\" keyword. +fassume-sane-operators-new-delete +C++ ObjC++ LTO Alias(fassume-sane-operators-new-delete=,2,0) +Assume C++ replaceable global operators new, new[], delete, delete[] don't read or write visible global state. + +fassume-sane-operators-new-delete= +C++ ObjC++ LTO Joined RejectNegative Var(flag_assume_sane_operators_new_delete) UInteger Init(1) IntegerRange(0, 2) +Assume C++ replaceable global operators new, new[], delete, delete[] don't read or write visible global state. + ; Define extra predefined macros for use in libgcc. fbuilding-libgcc C ObjC C++ ObjC++ Undocumented Var(flag_building_libgcc) --- gcc/gimple.cc.jj 2024-11-14 18:26:16.877459015 +0100 +++ gcc/gimple.cc 2024-11-15 16:24:13.093917285 +0100 @@ -1598,14 +1598,29 @@ gimple_call_fnspec (const gcall *stmt) such operator, then we can treat it as free. */ if (fndecl && DECL_IS_OPERATOR_DELETE_P (fndecl) - && DECL_IS_REPLACEABLE_OPERATOR (fndecl) - && gimple_call_from_new_or_delete (stmt)) - return ". o "; + && DECL_IS_REPLACEABLE_OPERATOR (fndecl)) + { + if (flag_assume_sane_operators_new_delete == 2) + return ".co "; + if (gimple_call_from_new_or_delete (stmt)) + { + if (flag_assume_sane_operators_new_delete == 1) + return ".po "; + return ". o "; + } + } /* Similarly operator new can be treated as malloc. */ - if (fndecl - && DECL_IS_REPLACEABLE_OPERATOR_NEW_P (fndecl) - && gimple_call_from_new_or_delete (stmt)) - return "m "; + if (fndecl && DECL_IS_REPLACEABLE_OPERATOR_NEW_P (fndecl)) + { + if (flag_assume_sane_operators_new_delete == 2) + return "mC"; + if (gimple_call_from_new_or_delete (stmt)) + { + if (flag_assume_sane_operators_new_delete == 1) + return "mP"; + return "m "; + } + } return ""; } --- gcc/doc/invoke.texi.jj 2024-11-15 13:13:29.031751688 +0100 +++ gcc/doc/invoke.texi 2024-11-15 16:06:17.270100613 +0100 @@ -213,7 +213,10 @@ in the following sections. @item C++ Language Options @xref{C++ Dialect Options,,Options Controlling C++ Dialect}. @gccoptlist{-fabi-version=@var{n} -fno-access-control --faligned-new=@var{n} -fargs-in-order=@var{n} -fchar8_t -fcheck-new +-faligned-new=@var{n} -fargs-in-order=@var{n} +-fno-assume-sane-operators-new-delete +-fassume-sane-operators-new-delete=@var{n} +-fchar8_t -fcheck-new -fconcepts -fconstexpr-depth=@var{n} -fconstexpr-cache-depth=@var{n} -fconstexpr-loop-limit=@var{n} -fconstexpr-ops-limit=@var{n} -fno-elide-constructors @@ -3162,6 +3165,48 @@ but few users will need to override the This flag is enabled by default for @option{-std=c++17}. +@opindex fno-assume-sane-operators-new-delete +@opindex fassume-sane-operators-new-delete +@opindex fassume-sane-operators-new-delete=@var{n} +@item -fno-assume-sane-operators-new +@itemx -fassume-sane-operators-new-delete=@var{n} +The C++ standard allows replacing the global @code{new}, @code{new[]}, +@code{delete} and @code{delete[]} operators, though a lot of C++ programs +don't replace them and just use the implementation provided version. +Furthermore, the C++ standard allows omitting those calls if they are +made from new or delete expressions (and by extension the same is +assumed if @code{__builtin_operator_new} or @code{__builtin_operator_delete} +functions are used). +These options allow control over some optimizations around calls +to those operators. +With @code{-fassume-sane-operators-new-delete} option or equivalent +@code{-fassume-sane-operators-new-delete=2} option GCC may assume that +calls to the replaceable global operators (whether from new or delete +expressions or called directly) don't read or modify any global variables +or variables whose address could escape to the operators (global state; +except for @code{errno} for the @code{new} and @code{new[]} operators). +This allows most optimizations across those calls and is something that +the implementation provided operators satisfy unless @code{malloc} +implementation details are observable in the code or unless @code{malloc} +hooks are used, but might not be satisfied if a program replaces those +operators. +With @code{-fno-assume-sane-operators-new-delete} option or +equivalent @code{-fassume-sane-operators-new-delete=0} option GCC must +assume all these calls (whether from new or delete expressions or called +directly) may read and write global state unless proven otherwise (e.g.@: +when GCC compiles their implementation). Use these options if those +operators are or may be replaced and code needs to expect such behavior. +With @code{-fassume-sane-operators-new-delete=1} option, which is +the default, GCC must assume reads and writes of global state for +direct calls to these operators, but if those calls are from new or +delete expressions or calls to @code{__builtin_operator_new} or +@code{__builtin_operator_delete}, it may assume global state is not +modified but might be read. This is because those calls could be +omitted by the implementation and if they would be omitted, code using +new or delete expressions can't rely on global state modifications +happening, while if they are not omitted, the replaced implementation +can still observe global state. + @opindex fchar8_t @opindex fno-char8_t @item -fchar8_t --- gcc/testsuite/g++.dg/tree-ssa/pr110137-1.C.jj 2024-11-15 16:16:56.296080508 +0100 +++ gcc/testsuite/g++.dg/tree-ssa/pr110137-1.C 2024-11-15 16:30:43.596407861 +0100 @@ -0,0 +1,75 @@ +// PR c++/110137 +// { dg-do compile } +// { dg-options "-O2 -fdump-tree-optimized" } +// { dg-final { scan-tree-dump "j = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump "m = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-times "q = 2;" 2 "optimized" } } */ +// { dg-final { scan-tree-dump-times "t = 2;" 2 "optimized" } } */ +// { dg-final { scan-tree-dump-not "k = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "n = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-times "r = 1;" 2 "optimized" } } */ + +int i, j, k, l, m, n, o, q, r, s, t, u; + +void * +foo () +{ + i = 1; + j = 2; + void *p = ::operator new (32); + j = 3; + k = i; + return p; +} + +void +bar (void *p) +{ + l = 1; + m = 2; + ::operator delete (p); + m = 3; + n = l; +} + +int * +baz () +{ + o = 1; + q = 2; + int *p = new int; + q = 3; + r = o; + return p; +} + +void +qux (int *p) +{ + s = 1; + t = 2; + delete p; + t = 3; + u = s; +} + +void * +corge () +{ + o = 1; + q = 2; + void *p = __builtin_operator_new (32); + q = 3; + r = o; + return p; +} + +void +waldo (void *p) +{ + s = 1; + t = 2; + __builtin_operator_delete (p); + t = 3; + u = s; +} --- gcc/testsuite/g++.dg/tree-ssa/pr110137-2.C.jj 2024-11-15 16:31:18.244919019 +0100 +++ gcc/testsuite/g++.dg/tree-ssa/pr110137-2.C 2024-11-15 16:31:40.919599111 +0100 @@ -0,0 +1,75 @@ +// PR c++/110137 +// { dg-do compile } +// { dg-options "-O2 -fdump-tree-optimized -fassume-sane-operators-new-delete=1" } +// { dg-final { scan-tree-dump "j = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump "m = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-times "q = 2;" 2 "optimized" } } */ +// { dg-final { scan-tree-dump-times "t = 2;" 2 "optimized" } } */ +// { dg-final { scan-tree-dump-not "k = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "n = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-times "r = 1;" 2 "optimized" } } */ + +int i, j, k, l, m, n, o, q, r, s, t, u; + +void * +foo () +{ + i = 1; + j = 2; + void *p = ::operator new (32); + j = 3; + k = i; + return p; +} + +void +bar (void *p) +{ + l = 1; + m = 2; + ::operator delete (p); + m = 3; + n = l; +} + +int * +baz () +{ + o = 1; + q = 2; + int *p = new int; + q = 3; + r = o; + return p; +} + +void +qux (int *p) +{ + s = 1; + t = 2; + delete p; + t = 3; + u = s; +} + +void * +corge () +{ + o = 1; + q = 2; + void *p = __builtin_operator_new (32); + q = 3; + r = o; + return p; +} + +void +waldo (void *p) +{ + s = 1; + t = 2; + __builtin_operator_delete (p); + t = 3; + u = s; +} --- gcc/testsuite/g++.dg/tree-ssa/pr110137-3.C.jj 2024-11-15 16:31:18.244919019 +0100 +++ gcc/testsuite/g++.dg/tree-ssa/pr110137-3.C 2024-11-15 16:33:16.099256260 +0100 @@ -0,0 +1,74 @@ +// PR c++/110137 +// { dg-do compile } +// { dg-options "-O2 -fdump-tree-optimized -fassume-sane-operators-new-delete" } +// { dg-final { scan-tree-dump-not "j = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "m = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "q = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "t = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump "k = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-times "r = 1;" 2 "optimized" } } */ + +int i, j, k, l, m, n, o, q, r, s, t, u; + +void * +foo () +{ + i = 1; + j = 2; + void *p = ::operator new (32); + j = 3; + k = i; + return p; +} + +void +bar (void *p) +{ + l = 1; + m = 2; + ::operator delete (p); + m = 3; + n = l; +} + +int * +baz () +{ + o = 1; + q = 2; + int *p = new int; + q = 3; + r = o; + return p; +} + +void +qux (int *p) +{ + s = 1; + t = 2; + delete p; + t = 3; + u = s; +} + +void * +corge () +{ + o = 1; + q = 2; + void *p = __builtin_operator_new (32); + q = 3; + r = o; + return p; +} + +void +waldo (void *p) +{ + s = 1; + t = 2; + __builtin_operator_delete (p); + t = 3; + u = s; +} --- gcc/testsuite/g++.dg/tree-ssa/pr110137-4.C.jj 2024-11-15 16:31:18.244919019 +0100 +++ gcc/testsuite/g++.dg/tree-ssa/pr110137-4.C 2024-11-15 16:33:39.034932674 +0100 @@ -0,0 +1,74 @@ +// PR c++/110137 +// { dg-do compile } +// { dg-options "-O2 -fdump-tree-optimized -fassume-sane-operators-new-delete=2" } +// { dg-final { scan-tree-dump-not "j = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "m = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "q = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "t = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump "k = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-times "r = 1;" 2 "optimized" } } */ + +int i, j, k, l, m, n, o, q, r, s, t, u; + +void * +foo () +{ + i = 1; + j = 2; + void *p = ::operator new (32); + j = 3; + k = i; + return p; +} + +void +bar (void *p) +{ + l = 1; + m = 2; + ::operator delete (p); + m = 3; + n = l; +} + +int * +baz () +{ + o = 1; + q = 2; + int *p = new int; + q = 3; + r = o; + return p; +} + +void +qux (int *p) +{ + s = 1; + t = 2; + delete p; + t = 3; + u = s; +} + +void * +corge () +{ + o = 1; + q = 2; + void *p = __builtin_operator_new (32); + q = 3; + r = o; + return p; +} + +void +waldo (void *p) +{ + s = 1; + t = 2; + __builtin_operator_delete (p); + t = 3; + u = s; +} --- gcc/testsuite/g++.dg/tree-ssa/pr110137-5.C.jj 2024-11-15 16:31:18.244919019 +0100 +++ gcc/testsuite/g++.dg/tree-ssa/pr110137-5.C 2024-11-15 16:35:24.354446761 +0100 @@ -0,0 +1,76 @@ +// PR c++/110137 +// { dg-do compile } +// { dg-options "-O2 -fdump-tree-optimized -fno-assume-sane-operators-new-delete" } +// { dg-final { scan-tree-dump "j = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump "m = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-times "q = 2;" 2 "optimized" } } */ +// { dg-final { scan-tree-dump-times "t = 2;" 2 "optimized" } } */ +// { dg-final { scan-tree-dump-not "k = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "n = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "r = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "u = 1;" "optimized" } } */ + +int i, j, k, l, m, n, o, q, r, s, t, u; + +void * +foo () +{ + i = 1; + j = 2; + void *p = ::operator new (32); + j = 3; + k = i; + return p; +} + +void +bar (void *p) +{ + l = 1; + m = 2; + ::operator delete (p); + m = 3; + n = l; +} + +int * +baz () +{ + o = 1; + q = 2; + int *p = new int; + q = 3; + r = o; + return p; +} + +void +qux (int *p) +{ + s = 1; + t = 2; + delete p; + t = 3; + u = s; +} + +void * +corge () +{ + o = 1; + q = 2; + void *p = __builtin_operator_new (32); + q = 3; + r = o; + return p; +} + +void +waldo (void *p) +{ + s = 1; + t = 2; + __builtin_operator_delete (p); + t = 3; + u = s; +} --- gcc/testsuite/g++.dg/tree-ssa/pr110137-6.C.jj 2024-11-15 16:31:18.244919019 +0100 +++ gcc/testsuite/g++.dg/tree-ssa/pr110137-6.C 2024-11-15 16:35:52.152054593 +0100 @@ -0,0 +1,76 @@ +// PR c++/110137 +// { dg-do compile } +// { dg-options "-O2 -fdump-tree-optimized -fassume-sane-operators-new-delete=0" } +// { dg-final { scan-tree-dump "j = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump "m = 2;" "optimized" } } */ +// { dg-final { scan-tree-dump-times "q = 2;" 2 "optimized" } } */ +// { dg-final { scan-tree-dump-times "t = 2;" 2 "optimized" } } */ +// { dg-final { scan-tree-dump-not "k = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "n = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "r = 1;" "optimized" } } */ +// { dg-final { scan-tree-dump-not "u = 1;" "optimized" } } */ + +int i, j, k, l, m, n, o, q, r, s, t, u; + +void * +foo () +{ + i = 1; + j = 2; + void *p = ::operator new (32); + j = 3; + k = i; + return p; +} + +void +bar (void *p) +{ + l = 1; + m = 2; + ::operator delete (p); + m = 3; + n = l; +} + +int * +baz () +{ + o = 1; + q = 2; + int *p = new int; + q = 3; + r = o; + return p; +} + +void +qux (int *p) +{ + s = 1; + t = 2; + delete p; + t = 3; + u = s; +} + +void * +corge () +{ + o = 1; + q = 2; + void *p = __builtin_operator_new (32); + q = 3; + r = o; + return p; +} + +void +waldo (void *p) +{ + s = 1; + t = 2; + __builtin_operator_delete (p); + t = 3; + u = s; +} Jakub