C++ Logo

std-proposals

Advanced search

Re: [std-proposals] std::elide

From: Breno Guimarães <brenorg_at_[hidden]>
Date: Wed, 22 May 2024 10:36:47 -0300
I think you drilled too much into the "mutex" and "locked" aspects, rather
than thinking of the immovable and post initialization.

You could have
void foo() {
    static auto mutex = getMutexTheOtherTeamToldMeToUse();
}

There are even other types that *are* movable but moving is not cheap.


On Wed, May 22, 2024 at 10:17 AM Tiago Freire via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> While I can understand RVO, mutexes has got to be the worst possible
> example for an application of this.
> It should just simply never happen in real code ever.
> I have posed this question before but got ignored, but it is crucial to
> the point.
>
> Why std::mutex can not be copied or moved (copy constructed or move
> constructed)?
> The reason for this is that some implementations of mutexes (posix mutexes
> are in this category), the object representation of the mutex itself is a
> piece of memory that lives in the user's space.
> And std::mutex accommodates this implementation.
> The exact address of this object is what makes a specific mutex that exact
> mutex.
> Moving or copying implies at least 2 distinct objects at different
> addresses, while in windows because it is implemented via a system handle
> you would be able to transfer the mutex representation, but not on posix.
> Copying data from one address onto another would change the state of the
> mutex that is being copied to, but it wouldn't be able to make the new
> mutex be the old mutex, because it is the address that defines the identity
> of the mutex, not the data.
>
> I'm not even mention the implications of moving a mutex object before
> destroying while it is potentially in use elsewhere.
>
>
> So, what you are proposing could only ever work like this:
>
> std::mutex m = getLockedMutex();
>
> and in the exclusive condition that RVO is taking place.
> If it doesn't the function itself would be ill-formed and so would the
> following code:
>
>
> std::mutex m;
> m = getLockedMutex();
>
>
> Ok, that's great, but why do you need to return a locked mutex again? Even
> without knowing what else it might be doing, assuming that the system in
> place was design sensibly, what is the bare minimum that we can deduce that
> this function must be doing.
>
> 1. It makes absolutely no sense to create a mutex if you are not using it.
> The bare minimum there must be another thread that uses it to synchronize
> with it.
> 2. And why would you need to return it locked? You would only do that if
> the mutex is protecting something, and it is locked because you don't want
> other threads accessing it. It means that other threads are already using
> it even before the function returns. If not, you could have just told the
> caller when to lock instead.
> 3. The mutex lives on the stack, specifically aligned at the point of
> where the caller as set out for it. So, whatever it is that the other
> thread is doing with this mutex, it must be resolved before this piece of
> code goes out of scope and the mutex is destroyed.
> 4. Because the mutex lives on the stack aligned at the point of the
> caller, but it is written in a way that the callee must be the one
> constructing this mutex. It is not like it has the option of choosing
> between mutex A or mutex B, the function needs to initialize this exact
> mutex at the caller's address and it can not return without valid
> constructed mutex.
> 5. The exact address of where in the stack this mutex resides is not
> easily predictable, so I don't think it would be sensible to do it by means
> other than reading the stack pointer at runtime. Otherwise, it couldn't
> make this specific mutex known to other threads.
>
> Even without knowing anything else about what this function is doing, this
> must be true:
> 1. The function must assume that the caller has prepared an address for
> the mutex
> 2. The function must (it is not optional) initialize the mutex in the
> memory space prepared by the caller
> 3. The function must complete the initialization before making it know to
> another user thread, and lock it (the order of making it known to another
> thread first or locking the mutex, which comes first is not determined)
> 4. And these must happen within the function before returning.
>
> Given that all of this must be set by the caller. There's absolutely no
> reason why the caller wouldn't want to just pass an already initialized
> mutex to the function instead.
> Even without knowing about anything else that this function might be doing.
>
> This:
> std::mutex m;
> getLockedMutex(m);
>
> Should have been the design.
> And all of this discussion about changing the language to accommodate
> returning a locked mutex, it just makes absolutely no sense from a
> principled design perspective.
> It's a bad example for RVO.
>
> It is trying to jump through hoops that have no reason to be jumped.
>
>
> -----Original Message-----
> From: Std-Proposals <std-proposals-bounces_at_[hidden]> On Behalf Of
> Frederick Virchanza Gotham via Std-Proposals
> Sent: Wednesday, May 22, 2024 12:04
> To: std-proposals_at_[hidden]
> Cc: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
> Subject: Re: [std-proposals] std::elide
>
> On Wed, May 22, 2024 at 4:00 AM Arthur O'Dwyer wrote:
> >
> > auto getLockedMutex(std::mutex *pm) {
> > Auto(pm->lock());
> > return std::mutex();
> > }
> >
> > int main() {
> > std::mutex m = getLockedMutex(&m);
> > m.unlock();
> > }
>
>
> I had to write this out step by step to understand it. So we start with:
>
> auto getLockedMutex(std::mutex *pm)
> {
> Auto(pm->lock());
> return std::mutex();
> }
>
> which is equivalent to:
>
> std::mutex getLockedMutex(std::mutex *pm)
> {
> Auto(pm->lock());
> return std::mutex();
> }
>
> which is equivalent to:
>
> std::mutex getLockedMutex(std::mutex *pm)
> {
> using std::mutex;
> struct S { mutex &m; S(mutex &arg) : m(arg) {} ~S(){ m.lock(); } }
> s(*pm);
> return mutex();
> }
>
> which is equivalent on x86_64 and most other platforms to:
>
> void getLockedMutex(std::mutex *arg0, std::mutex *arg1)
> {
> using std::mutex;
> struct S { mutex &m; S(mutex &arg) : m(arg) {} ~S(){ m.lock(); } }
> s(*arg1);
> ::new(arg0) std::mutex;
> }
>
> To be sure, I checked the assembler produced on GodBolt. The first and
> last function start off identically:
>
> push rbx
> mov rbx, rdi
> xorps xmm0, xmm0
> movups xmmword ptr [rdi + 16], xmm0
> movups xmmword ptr [rdi], xmm0
> mov qword ptr [rdi + 32], 0
> mov rdi, rsi
> call pthread_mutex_lock_at_PLT
> test eax, eax
> jne Something_Went_Wrong
> mov rax, rbx
> pop rbx
> ret
> Something_Went_Wrong:
> mov edi, eax
> call std::__throw_system_error(int)@PLT
>
> But the last function has two more instructions tagged onto the end:
>
> mov rdi, rax
> call __clang_call_terminate
>
> The difference is that in Arthur's original function, if "lock"
> throws, then the mutex must get destroyed before the function returns
> -- so then I looked through the assembler trying to find the call to the
> destructor. . . but I couldn't find it (then I realised that
> "mutex::~mutex" doesn't do anything). But there's another difference .
> . . if "lock" throws in Arthur's code, then std::terminate is called
> because an exception is thrown in the destructor of a local object, as
> demonstrated here:
>
> https://godbolt.org/z/xW1Y7hW4G
>
> (By the way I suggested to Arthur in an email a few months ago that he
> should enclose the destructor of Auto inside a "try/catch(...)").
>
> Let's see what happens though with a class type that has a destructor with
> an observable effect. So let's start off with:
>
> struct S {
> inline static volatile int n = 0;
> S(S const & ) = delete;
> S(S &&) = delete;
> S() __attribute__ ((noinline)) { n = 1; }
> ~S() __attribute__ ((noinline)) { n = 2; }
> void Throw(void) noexcept(false) __attribute__ ((noinline)) { if (
> 666 != n ) throw n; }
> };
>
> And then we have Arthur's style of function:
>
> S Func(S *p)
> {
> struct X { S &s; X(S &arg) : s(arg) {} ~X(){ s.Throw(); } } x(*p);
> return S();
> }
>
> And then we have my own style of function:
>
> void Func2_(S *arg0, S *arg1)
> {
> struct X { S &s; X(S &arg) : s(arg) {} ~X(){ s.Throw(); } }
> x(*arg1);
> ::new(arg0) S();
> }
>
> Arthur's style becomes:
>
> push r14
> push rbx
> push rax
> mov r14, rsi
> mov rbx, rdi
> call S::S() [base object constructor]
> mov rdi, r14
> call S::Throw()
> mov rax, rbx
> add rsp, 8
> pop rbx
> pop r14
> ret
> mov rdi, rax
> call __clang_call_terminate
>
> While my own style becomes:
>
> push rbx
> mov rbx, rsi
> call S::S() [base object constructor]
> mov rdi, rbx
> call S::Throw()
> pop rbx
> ret
> mov rdi, rax
> call __clang_call_terminate
>
> Neither of them invoke the destructor of S. If we were to edit Arthur's
> style to manually invoke the destructor as follows:
>
> S Func1(S *p)
> {
> struct X {
> S &s;
> X(S &arg) : s(arg) {}
> ~X(void)
> {
> try { s.Throw(); }
> catch(...) { s.~S(); throw; }
> }
> } x(*p);
> return S();
> }
>
> Then the assembler becomes:
>
> push r14
> push rbx
> push rax
> mov rbx, rsi
> mov r14, rdi
> call S::S() [base object constructor]
> mov rdi, rbx
> call S::Throw()
> mov rax, r14
> add rsp, 8
> pop rbx
> pop r14
> ret
> mov rdi, rax
> call __cxa_begin_catch
> mov rdi, rbx
> call S::~S() [base object destructor]
> call __cxa_rethrow
> mov rdi, rax
> call __clang_call_terminate
>
> This one is a little better as we can see the destructor getting called,
> but when we try to re-throw the exception, std::terminate gets called. So
> this isn't exception-safe.
>
> Of all the possible ways of achieving NRVO shared so far on this mailing
> list in order to return a locked mutex by value from a function, I think
> the best one so far is as follows: Write a function that returns a
> Derived<T> instead of a T, and then inside the constructor of Derived<T>,
> lock the mutex. Then take the address of the following function:
> Derived<T> Func(void);
> and cast it to:
> T (*)(void);
> Invoke the function pointer, and voila you get back a locked mutex.
> Maybe put in a few static_assert's to make sure that Derived<T> is
> equivalent in size and alignment to T.
>
> Or of course there's always the idea of giving us full access to the
> return slot: http://www.virjacode.com/papers/returnslot.htm
> --
> 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 2024-05-22 13:37:01