-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
representation for initializing expressions and putting function return values in registers #3133
Comments
Another consideration: should the expression category of a function call always be "initializing", or should it follow the "guaranteed in-place" nature of the call? We may be able to avoid materializing some temporaries if we say that the expression category depends on the type:
Also of note: there are definitely types for which it's reasonable for the value representation to be This suggests a refinement of the above rule: a type defaults to being returned in-place if either its value representation is not |
Isn't the relevant criteria whether the move constructor is a bit-copy?
|
Yes, I think instead of checking for a trivial destructor, we could check for a trivial destructive move. Either check seems OK to me, and it's plausible that we could encounter a type that has a trivial destructor but not trivial destructive move, so we could consider checking both properties. |
Why check both though? |
Because there can exist types that have trivial destruction but not a trivial destructive move. Eg, if a type logs every time an instance is created, but has a trivial destructor, we can still return it in a register, because we're returning a value, not an object. Edit: To be clear, we can return in registers if either condition holds. |
I don't think so: consider this C++ class:
Trivial destructor, but can't be returned in registers (since non-trivial destructive move / "not trivially relocatable"). I think the key issue is when the address of the object is captured, which isn't really related to whether the destructor is trivial. |
Hm, right. The relevant constraint in C++ is "trivial copy or move + trivial destructor", and I was hoping we could get away with dropping the first half since we're not actually constructing the new object as part of the return (that could happen in the caller depending on how they use the resulting value), but yeah, there's still a lifetime issue if the object holds pointers into itself. So perhaps "trivial destructive move" is the best we can do, and it's certainly logical and a good match to the C++ rule. |
I think there are two different non-in-place optimizations here, and they have different constraints:
I suspect optimization (1) is more interesting, because it avoids going through memory unnecessarily, but it probably happens a lot less often because, for most interesting user-defined types, the value representation will be a pointer, which means it always contains a handle to state whose lifetime is ending when the function returns. In the case where the type's object and value representation are the same, these two optimizations seem like they might collapse to the same thing, but I think they actually don't, because optimization (2) is materializing an object in the caller (including, for example, a requirement to call a destructor) and optimization (1) is not. |
Start treating function calls as initializing expressions instead of as value expressions. This required adding support for expression categories. Value bindings and temporary materialization conversions are created where necessary to transition between expression categories. For a function call with a return slot, we speculatively create a materialized temporary before the call and either commit to it or replace it with something else later, once we see how the function call expression is actually used. This change follows the direction suggested in #3133 for initializing expressions: depending on the return type of a function, the return value will either be initialized in-place or returned directly. This is visible in the semantics IR, which is a little unfortunate but is probably necessary as this is part of the semantics of the program. --------- Co-authored-by: Chandler Carruth <[email protected]>
@zygoloid could you explain (1), maybe with an example? It also isn't obvious that you can construct an initializing expression from a value representation. It would be particularly useful if you give an example that can be returned due to (1) but not (2). I feel like criteria for (2) captures "nothing cares about the address of this object", which is what we really care about, but maybe there is another reason a type won't have a trivial/bit-copy move? My assumption was that "having trivial/bit-copy move" did not mean that we would materialized a representation in memory in order to then copy the bits -- I thought that criteria was sufficient to be able to do everything in registers. |
It's a little hard to get to an example where optimization (1) is possible but (2) is not, because I think it requires the type to have a custom value representation, which isn't something I have a lot of experience with and examples of. Here's a contrived example: suppose we have a type Optimization (2) isn't possible for this type. It's not trivially relocatable, because the object representation contains a But optimization (1) is possible. A value binding for this type can outlive the object that it binds to, because an |
We triage inactive PRs and issues in order to make it easier to find active work. If this issue should remain active or becomes active again, please comment or remove the This issue is labeled |
We currently unconditionally permit
returned var
s in functions, and the implication seems to be that the assert in this testcase holds for any typeT
:But that's problematic: it means that a function like
F
that uses areturned var
(or, transitively, initializes its return object from a function that uses areturned var
) cannot return in registers, which means that our calling convention, at least for functions whose bodies we can't see, can't return in registers, even for types likei32
or()
.I think:
As a starting point, how about this:
T
whose value representation isconst T*
defaults to being returned in-place; any type whose value representation isconst T
defaults to being returned by copy.-> T
return uses the default for the type; a function that uses a-> var T
return guarantees to return in-place.returned var
notation is only permitted in a function that is guaranteed to return in-place, either because the function uses-> var
or (optionally) because the type requires an in-place return.The text was updated successfully, but these errors were encountered: