Date: Tue, 1 Mar 2022 23:26:17 +0100
> The larger picture here seems to be an effort to make move semantics friendlier and easier to use. I began an informal survey on open-std of papers on move semantics.
Move semantics so far suffer in 2 domains:
- There is no way in the language standard to notify the compiler that
an object is trivially deconstrutible as it hasn't been touched after
a specific class of constructor
- There is no way to tell the compiler that an object which has just
been moved shall be implicitly assumed to have returned to such a
pristine state.
In combination, unless the compiler can fully inline the object's
destructor as well as the foreign, receiving end of the move, it can't
*prove* that the object was trivial to destruct. You do not want
elimination of destruction for trivial states to rely solely on the
data flow analysis of fully inlined code though.
Being able to designate constructors as "creates trivially
destructible object" (which doesn't mean that the destructor musn't be
called, only that it can be safely omitted if the flag can be tracked
by pure data flow analysis, even prior to inlining) would be the first
step.
In the second step, it would need to be possible to also restore that
flag when accepting an object as r-value, giving the guarantee to the
caller that the object may be assumed to be trivially destructible
again.
For a trivially destructible object, the compiler may even shorten the
life cycle at will, or reuse it without construction of a new object,
if the guarantees are strong enough.
What guarantees are required for the life cycle optimizations?
- The moved object must not be aliased.
- The moved object must be *formally* equivalent to a pristine object
(even though *not* bitwise identical!) after the r-value expires.
What guarantees must an object give to be even able to be flagged "pristine"?
- The default constructor must explicitly yield a trivially destructible object.
- const methods must not introduce non-trivialy-destructible state to
the object.
Is this limited to move constructors? No, a quick thought experiment
shows this is actually universally applicable:
struct unique_ptr {
// Denotes the object to be trivially destructible when this
constructor was called.
// This property applies as long as the object remains pristine.
unique_ptr() noexcept [[post=trivial]] { /* non-trivial constructor */};
// Denotes that other turns trivial after the r-value reference expires
unique_ptr(unique_ptr&& [[post=trivial]] other);
// Denotes that a new object can be trivially default-constructed
in the released storage.
// Applies both if the destructor was called or omitted.
~unique_ptr() [[post=trivial]] { /* non-trivial destructor */};
// Denotes that this turns trivial again after invocation.
void reset() [[post=trivial]];
// Implicitly changes destruction behavior back to default.
void modify();
// Does not change the current destruction behavior.
operator bool() const noexcept();
// Explicitly gives up the trivial deconstruction and switches
back to destructor invocation.
void mutate() [[post=default]] const noexcept;
}
int main()
{
// Trivially destructible, non-trivial construction
unique_ptr foo_1;
// No longer trivial, deconstruction behavior reset to default
foo_1.modify();
// Explicitly made trivial again
foo_1.reset();
// Not used up to the end of the scope, trivial and not aliased,
so life cycle may now be shortened.
// Destructor call may be omitted.
// If the life cycle is shortened, the destructor *must not* be invoked!
// Since foo_1 was trivial till the end of the life cycle, foo_2
may reuse the storage!
// In that case, foo_2 was trivially constructible.
unique_ptr foo_2;
{
// Being aliased doesn't clear the "trivial" flag, but still
extends the life cycle.
// If alias can't be proven to expire, "aliased" property sticks.
// If alias can't be proven to preserve deconstruction
behavior, "trivial" flag is lost.
const auto& foo_3 = foo_2;
}
// foo_2 is no longer aliased, but non-trivial now.
foo_2.modify();
// foo_2 returns to trivial state, foo_4 is non-trivial
// std::move has a catch, it needs to convey the trivial attribute
applied to the r-value back to foo_2.
// It also mustn't accidentally set the aliased flag.
unique_ptr foo_4(std::move(foo_2));
// foo_2 may end its life-cycle without destructor invocation.
// foo_4 is guaranteed to have its destructor invoked under all
regular as-if rules.
}
No need for ABI changes though, it's all only about signalling the
caller that cleanup of a usually non-trivial object has just become
trivial, and providing the formal means to enable optimizations
without inlining.
And the behavior of the caller really must be controlled by the
callee, as only the callee can give any guarantees about upholding
code contracts. It's the exact same pattern as with constness.
Also, everything is only a recommendation / relaxation to the callee,
so 100% downwards compatible apart from new keywords.
The only thing which smells fishy? That all the other proposals all
attempted an imperative, caller controlled semantic, while I somehow
can't find any rationale to do so.
It's complementary with the "bitcopy move constructor" semantics, but
it's orthogonal to all the attempts to define any form of "relocation
with forced destruction".
Am Mi., 2. Feb. 2022 um 10:43 Uhr schrieb William Linkmeyer via
Std-Proposals <std-proposals_at_[hidden]>:
>
> The larger picture here seems to be an effort to make move semantics friendlier and easier to use. I began an informal survey on open-std of papers on move semantics.
>
> After reading unrelated papers, though, the thought occurred that move semantics will:
> 1. move semantics are becoming more implicit (defaulting to move where applicable) and unified (utilities for move-based alternatives in the language are becoming prevalent)
> 2. we should consider that, in a decade or so, move semantics may become more than a friendly memcpy/delete, and
> 3. papers on move semantics should be weighed against the requirements they may place *on* the ABI
>
> To illustrate the first point:
> - a proposal for (more) move semantics in views: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2446r1.html
> - a paper describing move semantics at scale (esp. in containers): http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2329r0.pdf
> - “proposes a conservative, move-only equivalent of std::function”: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p0288r9.html
> - a proposal for simpler implicit move in return statements (clarifying c++20’s implicit move): http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p1018r13.html#biblio-p2266r1
>
> The second point is speculative by its nature. Papers are often reflective on the past or aspirational for the relatively near future.
>
> I am proposing that, in a decade or so, it is not unlikely that:
> - memory will be far more distributed than it is today
> - processors, not processor cores, often of various types will share memory — such as a GPU sharing memory with the CPU, or a Docker Swarm of several computers
> - transactional memory will become more prevalent, perhaps becoming incorporated into the standard with a similar speed as move semantics are today
>
> To illustrate these speculations, here are some papers:
> - module distribution, which blurs the line between platform-specific source code and abstract packages: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2473r1.pdf
> - freestanding, embeddable c++: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2338r0.html
> - minimalist transactional memory: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p1875r2.pdf
>
> These papers are meant to illustrate a trend towards scale-independent processing with highly distributed programs.
>
> It would be prudent, therefore, to think in terms of patterns that are implementable in that context so as to avoid painful ABI breaks in the future, imposed on ourselves.
>
> I am merely urging people more fluent than myself to consider a set of move semantics generic enough to be future-proof on platforms where it *is* analogous to pass a reloc operator into a function that has a 50/50 chance of actually relocating it or not.
>
>
> WL
>
> On Feb 1, 2022, at 4:07 PM, Barry Revzin via Std-Proposals <std-proposals_at_[hidden]> wrote:
>
>
>
>
> On Tue, Feb 1, 2022 at 4:04 AM Gašper Ažman via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>> Hi Sebastien,
>>
>> you sure made a pretty long write-up! What I'm missing on the first skim-through is a thorough review of the currently published papers in the space and answers to the previously surfaced objections.
>>
>> Some of the papers in this space:
>> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1144r5.html
>> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4158.pdf
>> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1029r3.pdf
>> http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0023r0.pdf
>
>
> Also, for instance, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0308r0.html has a section on Pilfering. Boost.Json uses that approach, for instance (probably other stuff in Boost too, haven't checked).
>
> Barry
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
Move semantics so far suffer in 2 domains:
- There is no way in the language standard to notify the compiler that
an object is trivially deconstrutible as it hasn't been touched after
a specific class of constructor
- There is no way to tell the compiler that an object which has just
been moved shall be implicitly assumed to have returned to such a
pristine state.
In combination, unless the compiler can fully inline the object's
destructor as well as the foreign, receiving end of the move, it can't
*prove* that the object was trivial to destruct. You do not want
elimination of destruction for trivial states to rely solely on the
data flow analysis of fully inlined code though.
Being able to designate constructors as "creates trivially
destructible object" (which doesn't mean that the destructor musn't be
called, only that it can be safely omitted if the flag can be tracked
by pure data flow analysis, even prior to inlining) would be the first
step.
In the second step, it would need to be possible to also restore that
flag when accepting an object as r-value, giving the guarantee to the
caller that the object may be assumed to be trivially destructible
again.
For a trivially destructible object, the compiler may even shorten the
life cycle at will, or reuse it without construction of a new object,
if the guarantees are strong enough.
What guarantees are required for the life cycle optimizations?
- The moved object must not be aliased.
- The moved object must be *formally* equivalent to a pristine object
(even though *not* bitwise identical!) after the r-value expires.
What guarantees must an object give to be even able to be flagged "pristine"?
- The default constructor must explicitly yield a trivially destructible object.
- const methods must not introduce non-trivialy-destructible state to
the object.
Is this limited to move constructors? No, a quick thought experiment
shows this is actually universally applicable:
struct unique_ptr {
// Denotes the object to be trivially destructible when this
constructor was called.
// This property applies as long as the object remains pristine.
unique_ptr() noexcept [[post=trivial]] { /* non-trivial constructor */};
// Denotes that other turns trivial after the r-value reference expires
unique_ptr(unique_ptr&& [[post=trivial]] other);
// Denotes that a new object can be trivially default-constructed
in the released storage.
// Applies both if the destructor was called or omitted.
~unique_ptr() [[post=trivial]] { /* non-trivial destructor */};
// Denotes that this turns trivial again after invocation.
void reset() [[post=trivial]];
// Implicitly changes destruction behavior back to default.
void modify();
// Does not change the current destruction behavior.
operator bool() const noexcept();
// Explicitly gives up the trivial deconstruction and switches
back to destructor invocation.
void mutate() [[post=default]] const noexcept;
}
int main()
{
// Trivially destructible, non-trivial construction
unique_ptr foo_1;
// No longer trivial, deconstruction behavior reset to default
foo_1.modify();
// Explicitly made trivial again
foo_1.reset();
// Not used up to the end of the scope, trivial and not aliased,
so life cycle may now be shortened.
// Destructor call may be omitted.
// If the life cycle is shortened, the destructor *must not* be invoked!
// Since foo_1 was trivial till the end of the life cycle, foo_2
may reuse the storage!
// In that case, foo_2 was trivially constructible.
unique_ptr foo_2;
{
// Being aliased doesn't clear the "trivial" flag, but still
extends the life cycle.
// If alias can't be proven to expire, "aliased" property sticks.
// If alias can't be proven to preserve deconstruction
behavior, "trivial" flag is lost.
const auto& foo_3 = foo_2;
}
// foo_2 is no longer aliased, but non-trivial now.
foo_2.modify();
// foo_2 returns to trivial state, foo_4 is non-trivial
// std::move has a catch, it needs to convey the trivial attribute
applied to the r-value back to foo_2.
// It also mustn't accidentally set the aliased flag.
unique_ptr foo_4(std::move(foo_2));
// foo_2 may end its life-cycle without destructor invocation.
// foo_4 is guaranteed to have its destructor invoked under all
regular as-if rules.
}
No need for ABI changes though, it's all only about signalling the
caller that cleanup of a usually non-trivial object has just become
trivial, and providing the formal means to enable optimizations
without inlining.
And the behavior of the caller really must be controlled by the
callee, as only the callee can give any guarantees about upholding
code contracts. It's the exact same pattern as with constness.
Also, everything is only a recommendation / relaxation to the callee,
so 100% downwards compatible apart from new keywords.
The only thing which smells fishy? That all the other proposals all
attempted an imperative, caller controlled semantic, while I somehow
can't find any rationale to do so.
It's complementary with the "bitcopy move constructor" semantics, but
it's orthogonal to all the attempts to define any form of "relocation
with forced destruction".
Am Mi., 2. Feb. 2022 um 10:43 Uhr schrieb William Linkmeyer via
Std-Proposals <std-proposals_at_[hidden]>:
>
> The larger picture here seems to be an effort to make move semantics friendlier and easier to use. I began an informal survey on open-std of papers on move semantics.
>
> After reading unrelated papers, though, the thought occurred that move semantics will:
> 1. move semantics are becoming more implicit (defaulting to move where applicable) and unified (utilities for move-based alternatives in the language are becoming prevalent)
> 2. we should consider that, in a decade or so, move semantics may become more than a friendly memcpy/delete, and
> 3. papers on move semantics should be weighed against the requirements they may place *on* the ABI
>
> To illustrate the first point:
> - a proposal for (more) move semantics in views: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2446r1.html
> - a paper describing move semantics at scale (esp. in containers): http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2329r0.pdf
> - “proposes a conservative, move-only equivalent of std::function”: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p0288r9.html
> - a proposal for simpler implicit move in return statements (clarifying c++20’s implicit move): http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p1018r13.html#biblio-p2266r1
>
> The second point is speculative by its nature. Papers are often reflective on the past or aspirational for the relatively near future.
>
> I am proposing that, in a decade or so, it is not unlikely that:
> - memory will be far more distributed than it is today
> - processors, not processor cores, often of various types will share memory — such as a GPU sharing memory with the CPU, or a Docker Swarm of several computers
> - transactional memory will become more prevalent, perhaps becoming incorporated into the standard with a similar speed as move semantics are today
>
> To illustrate these speculations, here are some papers:
> - module distribution, which blurs the line between platform-specific source code and abstract packages: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2473r1.pdf
> - freestanding, embeddable c++: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2338r0.html
> - minimalist transactional memory: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p1875r2.pdf
>
> These papers are meant to illustrate a trend towards scale-independent processing with highly distributed programs.
>
> It would be prudent, therefore, to think in terms of patterns that are implementable in that context so as to avoid painful ABI breaks in the future, imposed on ourselves.
>
> I am merely urging people more fluent than myself to consider a set of move semantics generic enough to be future-proof on platforms where it *is* analogous to pass a reloc operator into a function that has a 50/50 chance of actually relocating it or not.
>
>
> WL
>
> On Feb 1, 2022, at 4:07 PM, Barry Revzin via Std-Proposals <std-proposals_at_[hidden]> wrote:
>
>
>
>
> On Tue, Feb 1, 2022 at 4:04 AM Gašper Ažman via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>> Hi Sebastien,
>>
>> you sure made a pretty long write-up! What I'm missing on the first skim-through is a thorough review of the currently published papers in the space and answers to the previously surfaced objections.
>>
>> Some of the papers in this space:
>> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1144r5.html
>> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4158.pdf
>> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1029r3.pdf
>> http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0023r0.pdf
>
>
> Also, for instance, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0308r0.html has a section on Pilfering. Boost.Json uses that approach, for instance (probably other stuff in Boost too, haven't checked).
>
> Barry
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
Received on 2022-03-01 22:26:29