Date: Mon, 19 Dec 2022 23:58:38 +0100
Hello again,
I briefly read through your comments and will answer tomorrow.
I just wanted to quickly share another approach on the ABI issue. I've
given a lot of thoughts in the last hours, and it's really headachy: I see
no satisfying solution if we put the opt-out attribute on the class /
relocation ctor level.
My suggestion is that declaring a relocation constructor is not enough to
trigger the ABI change. Instead, only if the type of the parameter is
relocate-only (no copy, no move ctor, only a reloc ctor) that the ABI is
enforced to be callee-destroy.
That way, there is simply no ABI break as there are no such types today.
And for the types that really need this feature (relocate-only types), then
it's guaranteed to be there. Also, we would no longer need the opt-out
attribute.
The only downside is that move instead of relocation will likely happen all
the time when reloc is used on a function parameter, with incompatible ABI.
But I doubt it would be a real problem, and removing the ABI dilemma will
surely help this proposal move forward.
Regards,
Sébastien
On Mon, Dec 19, 2022, 20:59 Edward Catmur <ecatmur_at_[hidden]> wrote:
>
>
> On Mon, 19 Dec 2022 at 18:55, Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
> wrote:
>
>> On Mon, Dec 19, 2022 at 12:05 PM Edward Catmur via Std-Proposals <
>> std-proposals_at_[hidden]> wrote:
>>
>>> On Mon, 19 Dec 2022 at 17:52, Giuseppe D'Angelo via Std-Proposals <
>>> std-proposals_at_[hidden]> wrote:
>>>
>>>>
>>>> I'm not really sure without reading something concrete, but this seems
>>>> to be falling the same bucket of synthesizing copies / moves /
>>>> relocations (P1144) (/ [[trivial_abi]]), i.e. things compose
>>>> "automatically" by default or you've got type traits to allow you to
>>>> choose a composition strategy.
>>>>
>>>
>>> Yes, I agree on that part, if you mean that they should compose
>>> automatically for defaulted special member functions (including for
>>> aggregates).
>>>
>>
>> The most recent syntax
>> optional(optional) [[no_parameter_reloc(is_no_parameter_reloc<T>)]];
>> variant(variant) [[no_parameter_reloc(is_no_parameter_reloc<Types> or
>> ...)]];
>> certainly seems to be gravitating toward the P1144 propagation solution,
>> as seen in my libc++ fork here for example:
>>
>>
>> https://github.com/llvm/llvm-project/commit/cc0addf5ab7f84a1a959addc2d69f6ac38d07e98#diff-bbea28f47b73848b21aca02ff43cea0dd32a070825ce786de3d9566bd7ed4717
>>
>>
>> https://github.com/llvm/llvm-project/commit/cc0addf5ab7f84a1a959addc2d69f6ac38d07e98#diff-2c05a5b3a75a81a99615776824dcdc7e612b081e88f024d389e83e834eb20f1e
>>
>> The difference I see between this and P1144 is that this is conflating
>> trivial_abi with trivial relocatability (even though there are types which
>> are trivially relocatable that we probably don't want to be trivial_abi,
>> such as `std::array<int, 1000>`
>>
>
> I'm not sure I understand your position here. std::array<int, 1000> *is*
> trivial, so it certainly has trivial ABI. And that's fine; ABI says that a
> class type that is too large to be passed in (some small number of)
> registers is passed by copying from some stack location to another. Trivial
> ABI just says to, constructors and destructors notwithstanding, pass the
> object in the manner that ABI prescribes for a trivial class type of the
> same layout.
>
> ) and even with non-trivial relocatability.
>>
>
> I see it as allowing to express the fine-grained distinction between
> several possibilities, some of which already exist of course:
>
> * trivial
> * trivial ABI
> * trivially relocatable but not trivial for the purposes of ABI
> * non-trivially relocatable
> * relocatable via copy/move+destroy
> * immovable
>
> It's almost like a "customizable trivial_abi" at this point — "take this
>> type and pass it by trivial_abi, except that each time the object teleports
>> from one memory address through a register to another memory address, run
>> *this* code."
>> And the problem I see with that is that the "teleport" operation is
>> really *two* operations: "slurp from memory into a register" on the
>> caller's side, and "blat from register back into memory" on the callee's
>> side. For types that are trivially relocatable, both operations are
>> trivial. For types that aren't trivially relocatable, it's unclear what
>> these two operations should actually do, and in particular whether the
>> in-register representation can be used directly at all.
>>
>
> Ah; no, that isn't how a nontrivial relocating constructor works. It
> doesn't place the object representation into registers like a trivial
> relocation (of a small enough class) would; it will in all likelihood have
> the same ABI as a copy or move constructor, i.e. taking two pointers, to
> the source and destination objects. (Although it has the additional effect
> of ending the lifetime of the source object.)
>
> And collapsing them both into `T(T)` (however it's ultimately spelled)
>> feels like a problem.
>>
>
> Well, the former is spelled `T(T) = default`. Those extra 2 keywords do a
> lot of work, here and elsewhere,
>
> Have you tried "manually implementing" this idea, by showing some simple
>> examples of `T(T)` and then showing the machine code generated from them?
>> Assume that nothing is inlined or optimized — so you're not allowed to
>> "cheat" by eliding any operations — just show exactly how the `T(T)`
>> constructor would be codegenned, and then what a call to it would look
>> like, and so on.
>>
>
> It would perhaps be more illuminating to show the equivalent C++ code.
> Given a type A that is relocatable through move+destroy, and a type T that
> contains an instance of A:
>
> struct A { A(A&&); ~A(); };
> struct T { A a; int i; };
>
> The automatically-generated (implicitly defaulted) T::T(T) would, modulo
> assumptions of ABI, be equivalent to the following (omitting exception
> handling, which Sébastien is certainly mindful of):
>
> void T_T_T(T* dest, T* src) {
> std::construct_at(&dest->a, std::move(src->a));
> src->a.~A();
> dest->i = src->i;
> }
>
> So there's really no need to compile down to machine code; the code
> executed by the relocating constructor is fully expressible in today's C++
> - though I should point out that I'm very much not advocating specifying it
> via source translation.
>
> HTH,
>> Arthur
>>
>
I briefly read through your comments and will answer tomorrow.
I just wanted to quickly share another approach on the ABI issue. I've
given a lot of thoughts in the last hours, and it's really headachy: I see
no satisfying solution if we put the opt-out attribute on the class /
relocation ctor level.
My suggestion is that declaring a relocation constructor is not enough to
trigger the ABI change. Instead, only if the type of the parameter is
relocate-only (no copy, no move ctor, only a reloc ctor) that the ABI is
enforced to be callee-destroy.
That way, there is simply no ABI break as there are no such types today.
And for the types that really need this feature (relocate-only types), then
it's guaranteed to be there. Also, we would no longer need the opt-out
attribute.
The only downside is that move instead of relocation will likely happen all
the time when reloc is used on a function parameter, with incompatible ABI.
But I doubt it would be a real problem, and removing the ABI dilemma will
surely help this proposal move forward.
Regards,
Sébastien
On Mon, Dec 19, 2022, 20:59 Edward Catmur <ecatmur_at_[hidden]> wrote:
>
>
> On Mon, 19 Dec 2022 at 18:55, Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
> wrote:
>
>> On Mon, Dec 19, 2022 at 12:05 PM Edward Catmur via Std-Proposals <
>> std-proposals_at_[hidden]> wrote:
>>
>>> On Mon, 19 Dec 2022 at 17:52, Giuseppe D'Angelo via Std-Proposals <
>>> std-proposals_at_[hidden]> wrote:
>>>
>>>>
>>>> I'm not really sure without reading something concrete, but this seems
>>>> to be falling the same bucket of synthesizing copies / moves /
>>>> relocations (P1144) (/ [[trivial_abi]]), i.e. things compose
>>>> "automatically" by default or you've got type traits to allow you to
>>>> choose a composition strategy.
>>>>
>>>
>>> Yes, I agree on that part, if you mean that they should compose
>>> automatically for defaulted special member functions (including for
>>> aggregates).
>>>
>>
>> The most recent syntax
>> optional(optional) [[no_parameter_reloc(is_no_parameter_reloc<T>)]];
>> variant(variant) [[no_parameter_reloc(is_no_parameter_reloc<Types> or
>> ...)]];
>> certainly seems to be gravitating toward the P1144 propagation solution,
>> as seen in my libc++ fork here for example:
>>
>>
>> https://github.com/llvm/llvm-project/commit/cc0addf5ab7f84a1a959addc2d69f6ac38d07e98#diff-bbea28f47b73848b21aca02ff43cea0dd32a070825ce786de3d9566bd7ed4717
>>
>>
>> https://github.com/llvm/llvm-project/commit/cc0addf5ab7f84a1a959addc2d69f6ac38d07e98#diff-2c05a5b3a75a81a99615776824dcdc7e612b081e88f024d389e83e834eb20f1e
>>
>> The difference I see between this and P1144 is that this is conflating
>> trivial_abi with trivial relocatability (even though there are types which
>> are trivially relocatable that we probably don't want to be trivial_abi,
>> such as `std::array<int, 1000>`
>>
>
> I'm not sure I understand your position here. std::array<int, 1000> *is*
> trivial, so it certainly has trivial ABI. And that's fine; ABI says that a
> class type that is too large to be passed in (some small number of)
> registers is passed by copying from some stack location to another. Trivial
> ABI just says to, constructors and destructors notwithstanding, pass the
> object in the manner that ABI prescribes for a trivial class type of the
> same layout.
>
> ) and even with non-trivial relocatability.
>>
>
> I see it as allowing to express the fine-grained distinction between
> several possibilities, some of which already exist of course:
>
> * trivial
> * trivial ABI
> * trivially relocatable but not trivial for the purposes of ABI
> * non-trivially relocatable
> * relocatable via copy/move+destroy
> * immovable
>
> It's almost like a "customizable trivial_abi" at this point — "take this
>> type and pass it by trivial_abi, except that each time the object teleports
>> from one memory address through a register to another memory address, run
>> *this* code."
>> And the problem I see with that is that the "teleport" operation is
>> really *two* operations: "slurp from memory into a register" on the
>> caller's side, and "blat from register back into memory" on the callee's
>> side. For types that are trivially relocatable, both operations are
>> trivial. For types that aren't trivially relocatable, it's unclear what
>> these two operations should actually do, and in particular whether the
>> in-register representation can be used directly at all.
>>
>
> Ah; no, that isn't how a nontrivial relocating constructor works. It
> doesn't place the object representation into registers like a trivial
> relocation (of a small enough class) would; it will in all likelihood have
> the same ABI as a copy or move constructor, i.e. taking two pointers, to
> the source and destination objects. (Although it has the additional effect
> of ending the lifetime of the source object.)
>
> And collapsing them both into `T(T)` (however it's ultimately spelled)
>> feels like a problem.
>>
>
> Well, the former is spelled `T(T) = default`. Those extra 2 keywords do a
> lot of work, here and elsewhere,
>
> Have you tried "manually implementing" this idea, by showing some simple
>> examples of `T(T)` and then showing the machine code generated from them?
>> Assume that nothing is inlined or optimized — so you're not allowed to
>> "cheat" by eliding any operations — just show exactly how the `T(T)`
>> constructor would be codegenned, and then what a call to it would look
>> like, and so on.
>>
>
> It would perhaps be more illuminating to show the equivalent C++ code.
> Given a type A that is relocatable through move+destroy, and a type T that
> contains an instance of A:
>
> struct A { A(A&&); ~A(); };
> struct T { A a; int i; };
>
> The automatically-generated (implicitly defaulted) T::T(T) would, modulo
> assumptions of ABI, be equivalent to the following (omitting exception
> handling, which Sébastien is certainly mindful of):
>
> void T_T_T(T* dest, T* src) {
> std::construct_at(&dest->a, std::move(src->a));
> src->a.~A();
> dest->i = src->i;
> }
>
> So there's really no need to compile down to machine code; the code
> executed by the relocating constructor is fully expressible in today's C++
> - though I should point out that I'm very much not advocating specifying it
> via source translation.
>
> HTH,
>> Arthur
>>
>
Received on 2022-12-19 22:58:50