Which of these pointer comparisons should a conforming compiler be able to optimize to "always false"?
Asked Answered
M

1

10

In an attempt to get a better understand of how pointer aliasing invariants manifested during optimization, I plugged some code into the renowned Compiler Explorer, which I'll repeat here:

#include <cstring>

bool a(int *foo, int *bar) {
    (void) *foo, (void) *bar;
    return foo == bar;
}

bool b(int *foo, float *bar) {
    (void) *foo, (void) *bar;
    return foo == reinterpret_cast<int *>(bar);
}

bool c(int *foo, int *bar) {
    (void) *foo, (void) *bar;
    // It's undefined behavior for memcpyed memory ranges to overlap (i.e. alias)
    std::memcpy(foo, bar, sizeof(int));
    return foo == bar;
}

bool d(int *__restrict foo, int *__restrict bar) {
    (void) *foo, (void) *bar;
    return foo == bar;
}

Neither the current versions of Clang nor GCC compile any of these functions to always return false, so my question is then which of these functions, while still complying with the C++ standard, could have been compiled to always return false? My (very limited) understanding says b, c, and d should all be optimizable in that manner, but I'm not confident (I also recognize that __restrict isn't in the standard, but pretending that it was with the semantics it's defined to have under either compiler).

Update

I've included dereferences of both pointers in the top of each function (so that they cannot be nullptr), and made the std::memcpy invocation actually copy one instance of int.

Update 2

Added a comment explaining my intent with the std::memcpy.

Merce answered 12/5, 2019 at 15:41 Comment(15)
b() can't, it's perfectly legal for both parameters to be nullptr without any strict aliasing violation.Minutes
Note that the G++ documentation says "As with all outermost parameter qualifiers, __restrict__ is ignored in function definition matching. This means you only need to specify __restrict__ in a function definition, rather than in a function prototype as well." and that pretty much wipes out any ability to deduce inequality at a call site (c())Minutes
@geza: Right, as long as alignment is compatible, pointers can be roundtripped though other pointer types perfectly legally. It's only when dereferenced that strict aliasing begins to apply.Minutes
Are the routines dereferencing the pointers in hope for a segfault if it dereferences a nullptr? Because that behavior is UB.Selfsustaining
@Eljay: The pointer dereferences are in there so that the compiler can assume that the pointers are non-null and point to valid memory (since otherwise dereferencing would be UB). That's the most portable way I know to communicate those invariants to the compiler.Merce
Note, as far as I know, nullptrs can be dereferenced. See cwg 232: open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#232Astral
@geza: But the resulting lvalue must never undergo lvalue-to-rvalue conversion... and (void)*foo (added in an edit) forces that conversion.Minutes
@BenVoigt: I'm not sure about this. What forces that conversion in (void)*foo?Astral
@geza: The C-style cast here performs a static_cast. A static_cast to void is a discarded-value expression A discarded-value expression involving indirection forces an lvalue-to-rvalue conversion only if it is also volatile So there is no lvalue-to-rvalue conversion here, one needs (void)*(volatile int*)foo.Minutes
d can't be optimised to return false. It's perfectly fine to pass the same pointer twice for a "restrict" pointer - it is only reading the same item through a restrict pointer and writing it in some different way which gives undefined behaviour.Pash
@Astral Actually null ptr can't be dereference; nothing allows that.Dandiprat
@curiousguy: it seems that you're right, the standard doesn't allow it. But that cwg issue seems to imply that this will change. But as it is old, maybe it won't. (as far as I remember, the when old standards needed an undefined behavior example, it said "nullptr dereference". But currently this is changed to "modifying a const variable" - this implies that the behavior of nullptr deref could change.)Astral
@Astral It really means that a few ppl handling the DR believe it should be defined, but clearly have not thought it through. There are a lot of very vague ideas in these DR discussions. Sometimes summary rejection of clearly valid reports and acceptance of purely bogus ones. In fact a lot of committee members have very vague and incomplete ideas. Often problems are solved by a handful of experts and voted by the majority who is impressed by the strength of their conclusion. (I have been there.)Dandiprat
@Astral In order for a null deref to not be undefined it needs a definition in all cases (where the result is not "used"); that means a description of these uses; obviously reading and writing a scalar is a use, but all types must be accounted and not even if the beginning of the discussion of what is a use can be found. Binding a ref? Does calling a member function constitute a use of a null lvalue? Why not, if you allow a null ref, you can allow a null this. Converting to a non virtual base class? It is more complicated than saying &*x is equivalent with x.Dandiprat
@curiousguy: Much of the "complexity" comes from a desire to avoid defining the behaviors of constructs which had once been widely if not universally treated as "popular extensions", but which compiler writers have pounced upon as UB in pursuit of "optimization". If one were to instead adopt an abstract machine model that's a bit closer to reality, and define behaviors in terms of abstract machine operations, the Standard could be much simpler.Twoply
E
2

For a it is obvious. For b the code is actually correct, the compiler cannot make any assumptions. Consider this call to b:

int x[2]{};
b(x,reinterpret_cast<float*>(x+1));

If you were accessing the value of the two parameters, maybe the compiler could make assumptions:

bool b(int *foo, float *bar) {
    *foo=10;  //*foo is an int (or unsigned int) 
              //and if foo is a member of a union 
              //*foo is the active member
    *bar+0.f; //bar must be a float within its lifetime so it cannot be
              //in the same union as *foo
    return foo == reinterpret_cast<int *>(bar);//so always false
    }

For c I agree with your analyze, a very smart compiler could optimize away the comparison.

For d, according to the C standard restrict only has implication on the way an object is accessed, not on the value of pointers see §6.7.3 in N1570

An object that is accessed through a restrict-qualified pointer has a special association with that pointer. This association, defined in 6.7.3.1 below, requires that all accesses to that object use, directly or indirectly, the value of that particular pointer.

As in the case of b if pointed object were accessed then a smart compiler could make assumptions:

bool d(int *__restrict foo, int *__restrict bar) {
  *foo=10;
  *bar=12;//So foo and bar point to different objects
  return foo == bar;//always false
  }
Eadith answered 12/5, 2019 at 17:16 Comment(14)
restrict only has implication - nee, doesn't 6.7.3.1p8 says that they can't alias the same object? And if we do accesses (void) *foo, (void) *bar;, the compiler can assert, that at least foo != bar?Maryjanemaryjo
@Maryjanemaryjo (void) *foo is not an access. See basic.lval/11 and expr.context/2Eadith
A clever compiler could "optimize" c() if its sole purpose was to be conforming, but that doesn't imply that a smart compiler could do so. Cleverness and stupidity are not antonyms. The Committee expected that implementations intended for various purposes would support "popular extensions" appropriate for such purposes; I cannot think of any purpose for which a compiler that would try to "optimize" c() would be more suitable than one which extends the language by treating memcpy like memmove when the source and destination match--an extension that would seldom cost anythingTwoply
@Twoply I agree with that. On the other hand, I have the feeling that coder want more and more the compiler to be able to understand their intent. The type system seems to be not enough to express this intent. Constraint imposed on value thanks to contract will help. And as soon as contract will be their coder will expect that code generation takes into account those constraints!Eadith
@Oliv: Programmer "intent" is simple: programmers intend that programs behave in a fashion consistent with how a compiler would behave if everything was volatile and the compiler was making a reasonable effort to the best code possible under that constraint. Further, even most programs that are incompatible with gcc/clang optimizers would be compatible with one that made anything resembling a bona fide effort to behave in the indicated fashion, rather than insisting that they're not required to do so.Twoply
@Oliv: If a language fails to provide ways by which a programmer can indicate that a compiler need not accommodate the possibility of X, the proper solution is to add such means to the language or opt-in compiler flags. Even if programmers would seldom need to do X, and even if compilers specialized for purposes that would never require X might reasonably assume programmers would never do X, the notion that quality general-purpose implementations should blithely make such assumptions is dangerous lunacy.Twoply
@Twoply The formal definition of UB [defns.undefined] clearly offers opportunity to implementation to specify their own special behavior in case of UB. But I think there have been a semantic shift during at least the last decade. Conferences of famous Clang developer presenting UB as an optimization opportunity have probably play a significant role in this semantic shift. Moreover, it looks like implementations tend not to document their specific behavior when the standard does not define the behavior ...Eadith
(implicitly or explicitly). I can not imagine that this semantic shift will be reverted. And I suspect that it is percolating in the standard. I agree this Q&A are formally wrong, because it is supposed what is the consensus now: code are compiled considering that UB will never happen, "no UB" is a premises of all expressions.Eadith
@Oliv: Not only does the definition allow it, but the authors of the Standard have stated quite clearly in the published Rationale that they expected implementations to do so, with the question of precisely when to do so being a quality-of-implementation issue which should be resolved by the marketplace rather than the Committee. Perhaps what's needed is a retronym to distinguish the language the Standard was written for the purpose of describing, versus the language that it actually defines.Twoply
@Twoply Your not going to like this paper: P1093. Percolation is done.Eadith
@Oliv: There is an increasing divergence between the language the authors of clang and gcc want to process, which is suitable for only a few specialized purposes that don't involve handling any data from untrustworthy sources, and the language which became popular in the 1980s and is suitable for many more purposes. Having a retronym to use in cases where the more-broadly-useful language is required would allow people to make their requirement clear.Twoply
@Twoply It makes sense. There is a c++ standardization commitee that is treating UB: SG12. Maybe it is possible to contact them, and latter write a paper. Or maybe you are already a commitee member. I know that there are few of them here: Barry, Columbo, probably other...Eadith
@Oliv: I'm not a committee member; I'll check out that link and see about the mailing list. Any idea how much traffic is there? The approach I'd like to see the Committee take would be to officially recognize that the Standard has always deliberately allowed implementations intended for specialized purposes to behave in ways that would make them inappropriate for many other purposes, and that an implementation may simultaneously be of high-quality for some purposes but unusably low quality for others. Such an approach would allow everyone to save face...Twoply
@Oliv: ...since clang and gcc could claim that their product is a high quality implementation for the purposes their maintainers are interested in, while those whose code is broken by clang/gcc could claim that it will work on high-quality implementations for their intended purposes. Having the Standard then not only recognize that implementations intended different purposes should offer different behavioral guarantees, but recognize means by which they can do so, would allow the language to be re-unified.Twoply

© 2022 - 2024 — McMap. All rights reserved.