C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Relocation in C++

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Tue, 20 Dec 2022 10:19:20 +0100
On Mon, 19 Dec 2022 at 23:58, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:

> 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.
>

Yes, that's another option. Really, it's up to the implementation what they
want to do about ABI:

* full break, use callee-destroy for all relocatable types (for those who
don't care about ABI)
* break with opt-out - an attribute to mark opt-out, and propagation
mechanisms (these could be standardized at a later date)
* no break, but opt-in - an improved [[trivial_abi]] that actually checks
that the type is trivially relocatable
* no break, use callee-destroy only for relocate-only types.
* and those that are callee-destroy already don't need to do anything!

And more or less orthogonal to that is whether to use trivial ABI for
trivially-relocatable nontrivial types passed as callee-destroy, or to
always pass such types on the stack. An implementation could also choose
something more complex such as passing trivially-relocatable types with
trivial ABI, but non-relocate-only nontrivially-relocatable types as
caller-destroy.

Given that there are so many options (and maybe more that I've missed),
maybe it would be best just to list them as a sign that we've considered
ABI as an issue. If one of them gets settled on across the current
caller-destroy platforms (are there any other than Itanium?), that can be
standardized (along with propagation mechanisms, if appropriate) at a later
date.

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-20 09:19:32