C++ Logo

std-proposals

Advanced search

Re: [std-proposals] std::elide

From: Tiago Freire <tmiguelf_at_[hidden]>
Date: Wed, 22 May 2024 13:17:37 +0000
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_lists.isocpp.org> On Behalf Of Frederick Virchanza Gotham via Std-Proposals
Sent: Wednesday, May 22, 2024 12:04
To: std-proposals_at_lists.isocpp.org
Cc: Frederick Virchanza Gotham <cauldwell.thomas_at_gmail.com>
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].org
https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

Received on 2024-05-22 13:17:43