[cfe-dev] Can indirect class parameters be noalias?

David Rector via cfe-dev cfe-dev at lists.llvm.org
Sun Aug 2 14:37:26 PDT 2020


# Regarding what change to the standard should be proposed:

I think what John is after is to make it undefined behavior to escape a pointer in the constructor of a trivially-copyable class.

Or, maybe it can be a bit more limited: you are allowed to escape the pointer, and e.g. use it to detect whether a copy was made — I don’t think *observing* whether a copy was made is necessarily problematic — but *dereferencing* the escaped pointer, i.e. peeking at the data it points to, should be undefined behavior.  

I.e. the key thing is to make Richard’s example undefined behavior — correct me if I’m wrong.

If we had this guarantee, we could mark any trivially-copyable-class-type’d parameters "noalias" and reap the optimization benefits in C and C++ alike.

But it would *also* be good, as I believe Hal suggests, to be able to specify attributes on parameters to e.g. guarantee a copy is made.  That would provide a very useful side benefit: whenever a user manually forces a copy in this way, they can be assured that any escaped pointer usage is back to being well-defined/supported (since a trivial copy is always guaranteed to be noalias).  

In other words, whenever you want to still be allowed to escape a pointer in a constructor for class `A` and remain standard-compliant, all you would need to do is change any `f(A a)` to something like `f([[forcecopy]] A a)`.  

We could also provide an alternative [[maybealias]] attribute which does not force a copy, but does force IRGen to omit the noalias marking, i.e. to be pessimistic.

Bottom line any weird cases can still be allowed to be standard compliant, but they would have to address the weirdness of the behavior they desire by explicitly telling the compiler what it can and cannot do, as I believe Hal suggested.  

This would definitely be a good thing, by a) making weird cases more visible to the eye while b) allowing better optimization of more typical cases.

--

# Regarding James’ example: 

This implicates aliasing as well, without ever dealing in escaped pointers — `foo_` is a potential alias of `f` in `CreateFoo()` whenever it is constructed via `foo_(CreateFoo())`.  While I don’t think this can affect the noalias status of function parameter types, which John is mainly concerned with, it seems like it should addressed in any solution to the parameter issue since it is so similar.

I am not an expert on the particulars of noalias, NRVO, or what the standard currently has to say about this, but the issue seems to be that whenever we modify object data using the returned value of a method like `CreateFoo()`, and allow it to use NRVO to modify the object data indirectly, at its ultimate destination, rather than working on a copy first, there will be potential aliasing problems due to the visibility of this and the sub-object pointers within the method.

However I think these issues are apart from the matter of whether the assert should always hold — I think it’s valid for the `assert(foo_.x == 55);` to sometimes hold and sometimes not, as it could be used to detect whether a copy was made, which could be of interest to the user.

So, it seems to me that the compiler needs to either 
assume the variable to be “returned” (`Foo f;` in the body of `CreateFoo()`) in such a method is *not* noalias, or 
disable NRVO, i.e. force a copy, whenever writing the “returned” value of a class method to data within that class. 
I.e. you can do one optimization or the other, but not both.

--

# For any struggling to follow the discussion, here is my (limited) understanding of the problem John initially raised:

Given a call `f(A(...))` to a function `f(A a)`, in constructing argument `a` the compiler implementation will always begin by building a temporary of the argument object using the constructor call `A(…)`.  

But then, the C++ standard allows some leeway: If `A` is trivially copyable, the implementation may build zero or more copies of that object before settling on the one to which &a will point.  (If `A` is not trivially copyable, it cannot make any such copies, so there is no such leeway.)

So, in some implementations `&a` may point to that initial temporary; in others it may point to a copy of that temporary.

This flexibility only becomes a problem if the pointer to the initial constructed temporary is allowed to "escape" from its constructor; i.e. if the constructor reveals the object’s `this` pointer, or one if its sub-object pointers, to the outside world, such that other code can depend on that pointer.

The standard apparently currently supports using an escaped pointer to a temporary however the user wishes.

Unfortunately this support creates a barrier to optimization: should an implementation decide to make *no* copies of the temporary argument object initially constructed via `A(...)`, such that `&a` = the address of the initial temporary, i.e. `&a` = the address which might have been escaped during construction of the temporary, in a perfectly standard-compliant way, then the compiler is forced to account for the possibility that the user has obtained `&a` before the function call and might be referring into `a`’s data in the body of `f` under some "alias" (e.g. `p` in Richard’s example).

To account for this possibility CodeGen must tread carefully and pessimistically, foregoing optimizations which could be used if only we could guarantee either that:
`a` was "noalias" or 
any path by which an alias of `a` arose is not standard-compliant and thus okay to disregard.

In practice, this means we want to either: 
Force to compiler to *always* make a copy when constructing `a`, solely for the purpose of giving `a` a fresh address which nothing else could have possibly seen (though if optimization is the goal, forcing ourselves to make a needless copy seems a step in the wrong direction), or 
Guarantee that any path which exposes the address of the temporary (or, at least, which dereferences such an address) is non-standard-compliant, i.e. declare that any "alias" of `a` is by its nature invalid, so that we can safely mark `a` as noalias.  

Option 1 is what clang and other compilers seem to currently be doing, as Richard suggests (and disapproves of), but Option 2 (which I believe is what John is after) seems the most reasonable, since I cannot think of a good reason anyone should need to use the actual data content of that initial temporary.  

But as suggested above, even if a user *does* have a good reason, and really does want to dereference an escaped pointer while remaining standard compliant, we could just require them to add something like a [[forcecopy]] or [[maybealias]] attribute to the parameter.  

So, hard to see how anyone would lose with this proposed change.

- Dave


> On Jul 31, 2020, at 7:50 PM, Hal Finkel via cfe-dev <cfe-dev at lists.llvm.org> wrote:
> 
> 
> 
> On 7/31/20 5:59 PM, James Y Knight wrote:
>> This discussion reminds me of an example I ran into a couple weeks ago, where the execution of the program is dependent precisely upon whether the ABI calls for the object to be passed indirectly, or in a register
>> 
>> In the case where NVRO is triggered, the class member foo_ is fully-constructed on the first line of CreateFoo (despite appearing as if that's only constructing a local variable). In the case where the struct is small enough to fit in a register, NVRO does not apply, and in that case, foo_ isn't constructed until after CreateFoo returns.
>> 
>> Therefore, I believe it's implementation-defined whether the following program has undefined behavior.
>> 
>> https://godbolt.org/z/YT9zsz <https://godbolt.org/z/YT9zsz>
>> 
>> #include <assert.h>
>> 
>> struct Foo {
>>     int x;
>>     // assert fails if you comment out these unused fields!
>>     int dummy[4];
>> };
>> 
>> struct Bar {
>>     Bar() : foo_(CreateFoo()) {}
>> 
>>     Foo CreateFoo() {
>>         Foo f;
>>         f.x = 55;
>>         assert(foo_.x == 55);
>>         return f;
>>     }
>>     Foo foo_;
>> };
>> 
>> int main() {
>>     Bar b;
>> }
> 
> Looks that way to me too. The example in 11.10.5p2 sort of makes this point as well (by pointing out that you can directly initialize a global this way).
> 
>  -Hal
> 
> 
> 
>> 
>> On Fri, Jul 31, 2020 at 2:27 PM Hal Finkel via cfe-dev <cfe-dev at lists.llvm.org <mailto:cfe-dev at lists.llvm.org>> wrote:
>> 
>> 
>> On 7/31/20 1:24 PM, Hal Finkel wrote:
>>> On 7/31/20 12:43 PM, John McCall wrote:
>>>> n 31 Jul 2020, at 7:35, Hal Finkel wrote:
>>>> 
>>>> On 7/29/20 9:00 PM, John McCall via cfe-dev wrote:
>>>> 
>>>> On 29 Jul 2020, at 17:42, Richard Smith wrote:
>>>> 
>>>> On Wed, 29 Jul 2020 at 12:52, John McCall <rjmccall at apple.com> <mailto:rjmccall at apple.com> wrote:
>>>> 
>>>> ...
>>>> 
>>>> I think concretely, the escape hatch doesn't stop things from
>>>> going wrong,
>>>> because -- as you note -- even though we *could* have made a copy,
>>>> it's
>>>> observable whether or not we *did* make a copy. For example:
>>>> 
>>>> I would say that it’s observable whether the parameter variable has
>>>> the same address as the argument. That doesn’t /have/ to be the same
>>>> question as whether a copy was performed: we could consider there to be
>>>> a formal copy (or series of copies) that ultimately creates /an/ object
>>>> at the same address, but it’s not the /same/ object and so pointers
>>>> to the old object no longer validly pointer to it. But I guess that
>>>> would probably violate the lifetime rules, because it would make accesses
>>>> through old pointers UB when in fact they should at worst access a valid
>>>> object that’s just unrelated to the parameter object.
>>>> 
>>>> I think that it would be great to be able to do this, but unfortunately, I think that the point that you raise here is a key issue. Whether or not the copy is performed is visible in the model, and so we can't simply act as though there was a copy when optimizing. Someone could easily have code that looks like:
>>>> 
>>>> Foo DefaultX;
>>>> 
>>>> ...
>>>> 
>>>> void something(Foo &A, Foo &B) {
>>>> 
>>>>   if (&A == &B) { ... }
>>>> 
>>>> }
>>>> 
>>>> void bar(Foo X) { something(X, DefaultX); }
>>>> 
>>>> This example isn’t really on point; a call like bar(DefaultX) obviously cannot just pass the address of DefaultX as a by-value argument without first proving a lot of stuff about how foo uses both its parameter and DefaultX. I think noalias is actually a subset of what would have to be proven there.
>>>> 
>>> 
>>> Yes, I apologize. You're right: my pseudo-code missed the point. So the record is clear, let me rephrase:
>>> 
>>> Foo *DefaultX = nullptr;
>>> ...
>>> Foo::Foo() { if (!DefaultX) DefaultX = this; }
>>> ...
>>> void bar(Foo X) { something(X, *DefaultX); }
>>> ...
>>> bar(Foo{});
>>> I think that's closer to what we're talking about.
>>> 
>>> 
>>> 
>>>> In general, the standard is clear that you cannot rely on escaping a pointer to/into a trivially-copyable pr-value argument prior to the call and then rely on that pointer pointing into the corresponding parameter object. Implementations are allowed to introduce copies. But it does seem like the current wording would allow you to rely on that pointer pointing into some valid object, at least until the end of the caller’s full-expression. That means that, if we don’t guarantee to do an actual copy of the argument, we cannot make it UB to access the parameter variable through pointers to the argument temporary, which is what marking the parameter as noalias would do.
>>>> 
>>>> So I guess the remaining questions are:
>>>> 
>>>> Is this something we can reasonably change in the standard?
>>> 
>>> This is the part that I'm unclear about. What change would we make? 
>>> 
>>> 
>>> 
>> 
>> Also, maybe some extended use of the no_unique_address attribute would help?
>> 
>>  -Hal
>> 
>> 
>> 
>>> 
>>> 
>>>> Are we comfortable setting noalias in C if the only place that would break is with a C++ caller?
>>> 
>>> Out of curiosity, if you take C in combination with our statement-expression extension implementation (https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html <https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html>), and notwithstanding the statement in the GCC manual about returns by value (i.e., the part just before where it says, "Therefore the this pointer observed by Foo is not the address of a."), is there any relationship to this topic?
>>> 
>>> Thanks again,
>>> 
>>> Hal
>>> 
>>> 
>>> 
>>>> John.
>>>> 
>>>> As Richard's example shows, the code doesn't need to explicitly compare the addresses to detect the copy either. Any code that reads/writes to the objects can do it. A perhaps-more-realistic example might be:
>>>> 
>>>>   int Cnt = A.RefCnt; ++A.RefCnt; ++B.RefCnt; if (Cnt + 1 != A.RefCnt) { /* same object case */ }
>>>> 
>>>> The best suggestion that I have so far is that we could add an attribute like 'can_copy' indicating that the optimizer can make a formal copy of the argument in the callee and use that instead of the original pointer if that seems useful. I can certainly imagine a transformation such as LICM making use of such a thing (although the cost modeling would probably need to be fairly conservative).
>>>> 
>>>>  -Hal
>>>> 
>>>> 
>>>> ...
>>>> 
>>>> John.
>>>> 
>>>> 
>>>> _______________________________________________
>>>> cfe-dev mailing list
>>>> cfe-dev at lists.llvm.org <mailto:cfe-dev at lists.llvm.org>
>>>> https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev <https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev>
>>>> -- 
>>>> Hal Finkel
>>>> Lead, Compiler Technology and Programming Languages
>>>> Leadership Computing Facility
>>>> Argonne National Laboratory
>>>> 
>>> -- 
>>> Hal Finkel
>>> Lead, Compiler Technology and Programming Languages
>>> Leadership Computing Facility
>>> Argonne National Laboratory
>> -- 
>> Hal Finkel
>> Lead, Compiler Technology and Programming Languages
>> Leadership Computing Facility
>> Argonne National Laboratory
>> _______________________________________________
>> cfe-dev mailing list
>> cfe-dev at lists.llvm.org <mailto:cfe-dev at lists.llvm.org>
>> https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev <https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev>
> -- 
> Hal Finkel
> Lead, Compiler Technology and Programming Languages
> Leadership Computing Facility
> Argonne National Laboratory
> _______________________________________________
> cfe-dev mailing list
> cfe-dev at lists.llvm.org
> https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/cfe-dev/attachments/20200802/6586e874/attachment-0001.html>


More information about the cfe-dev mailing list