Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace UnsafeCell with MaybeUnit #53

Closed
wants to merge 3 commits into from

Conversation

jannic
Copy link
Member

@jannic jannic commented Sep 19, 2024

This leads to slightly better code generation in some cases where the mutex contents are accessed multiple times in a row.

Closes: #52

@jannic jannic requested a review from a team as a code owner September 19, 2024 08:33
@thalesfragoso
Copy link
Member

Thanks for taking the time to investigate and create this PR.

We will have to also add a Drop implementation with assume_init_drop. I guess this is also non-breaking, since I think this would only break code that tries to destructure the Mutex, which isn't possible because the field is private.

This leads to slightly better code generation in some cases where the
mutex contents are accessed multiple times in a row.
@jannic jannic force-pushed the mutex-with-maybeuninit branch from 8cb9ec9 to f1e3b70 Compare September 19, 2024 11:04
@jannic jannic marked this pull request as draft September 19, 2024 11:34
@Dirbaio
Copy link
Member

Dirbaio commented Sep 19, 2024

I'm not sure we should do this.

The only advantage AFAICT is better codegen when the contents are inmutable. I.e. Mutex<u32> and not Mutex<Cell<u32>>. However, what's the use case for that? It seems to me you can always replace a Mutex<u32> with just a u32.

When the mutex content does have interior mutability (like Cell or RefCell) the optimization will be prevented anyway by the inner RefCell, so we gain nothing by removing the outer RefCell. (and the optimization has to be prevented, because it's incorrect when you do have mutability).

So there's no codegen improvements for the way Mutex is actually used in the wild, is there?

and there are a few downsides:

  • MaybeUninit feels the wrong tool for the job, the contents can never be actually uninit.
  • Due to the above, the code ends up a bit more complex: it equires a manual drop impl now, and two more unsafe {} blocks (into_inner, and Drop).
  • I still haven't seen a convincing explanation for why removing this UnsafeCell is sound.

@jannic jannic force-pushed the mutex-with-maybeuninit branch from f1e3b70 to ea9408d Compare September 19, 2024 13:17
@jannic
Copy link
Member Author

jannic commented Sep 19, 2024

The only advantage AFAICT is better codegen when the contents are inmutable. I.e. Mutex<u32> and not Mutex<Cell<u32>>. However, what's the use case for that? It seems to me you can always replace a Mutex<u32> with just a u32.

It can actually improve the generated code in case T contains some pointer - e.g. to a register. (That would also explain why it's wrapped in a Mutex in the first place, even though it's immutable: To avoid concurrent access to the register.)

In that case, the compiler can sometimes avoid a load instruction: https://godbolt.org/z/x8j4zn4Mb

So there's no codegen improvements for the way Mutex is actually used in the wild, is there?

Indeed I don't know if this is a common pattern.

and there are a few downsides:

* MaybeUninit feels the wrong tool for the job, the contents can never be actually uninit.

Sure, it's a workaround to say "don't use niches in here". Is there a better way.

* I still haven't seen a convincing explanation for why removing this UnsafeCell is sound.

How could it not be sound? UnsafeCell is only needed for interior mutability.

For any given T, one of two things must be true:

  • Either T does provide interior mutability (ie. its internal representation can change even if a &T reference exists). In this case, it must already be or contain an UnsafeCell. So an additional UnsafeCell layer in Mutex<T> wouldn't change anything.
  • Or T does not provide interior mutability at all. Then we know that, as long as we have access to a &Mutex<T>, the memory representation will not change. In that case, UnsafeCell is not needed and doesn't provide any advantages.

@jannic jannic marked this pull request as ready for review September 19, 2024 13:46
@jannic
Copy link
Member Author

jannic commented Sep 19, 2024

Alternatively, the comment https://github.com/rust-embedded/critical-section/pull/53/files#diff-71560e745278a3bdedc2c5684fa201e28b80663af84e25e82352cb8f5315610bR78-R85 would also fit for the current implementation, just replace MaybeUninit by UnsafeCell. That would answer #52 as well.

@Dirbaio
Copy link
Member

Dirbaio commented Sep 19, 2024

contains some pointer - e.g. to a register. (That would also explain why it's wrapped in a Mutex in the first place, even though it's immutable: To avoid concurrent access to the register.)

If the pointer is Copy, this is a rather weak protection, it's easy to bypass by accident with let ptr = critical_section::with(|cs| *mutex.borrow(cs)); ptr.write(...);.

I could imagine a HAL struct that contains the pointer, and has &self methods that is Send but not Sync because the methods aren't threadsafe. A user might wrap it with a Mutex to be able to call these methods from different priorities, yes. Interesting. I wonder if anyone does this in practice tho?

Sure, it's a workaround to say "don't use niches in here". Is there a better way.

UnsafeCell also does that :)

How could it not be sound? UnsafeCell is only needed for interior mutability. (...)

yeah, maybe! It's just that UnsafeCell has so much extra spooky magic that scares me.

UnsafeCell is needed to obtain a &mut from a &. You can't simply transmute references, even if you're manually ensuring only one &mut exists at a time. You have to "inform" the compiler you're doing special stuff by using UnsafeCell.

So, is UnsafeCell needed to obtain a &SomethingSync from a &SomethingSend? In this case it is OK to manually ensure only one thread has &SomethingSend a time, no need to "inform" the compiler you're doing special stuff anymore? Why?

@jannic
Copy link
Member Author

jannic commented Sep 19, 2024

contains some pointer - e.g. to a register. (That would also explain why it's wrapped in a Mutex in the first place, even though it's immutable: To avoid concurrent access to the register.)

If the pointer is Copy, this is a rather weak protection, it's easy to bypass by accident with let ptr = critical_section::with(|cs| *mutex.borrow(cs)); ptr.write(...);.

I could imagine a HAL struct that contains the pointer, and has &self methods that is Send but not Sync because the methods aren't threadsafe. A user might wrap it with a Mutex to be able to call these methods from different priorities, yes.

Sure - exactly as shown in the example in the compiler exporer link:

pub struct Register {
    ptr: *mut u32,
}

impl Register {
    fn read(&self) -> u32 {
        unsafe { core::ptr::read_volatile(self.ptr) }
    }
}

Interesting. I wonder if anyone does this in practice tho?

🤷

Sure, it's a workaround to say "don't use niches in here". Is there a better way.

UnsafeCell also does that :)

I know, but it's not a "better way". It has the same disadvantage that it's meant for something else (interior mutability) that we don't need here, and is only used for this side-effect.

How could it not be sound? UnsafeCell is only needed for interior mutability. (...)

yeah, maybe! It's just that UnsafeCell has so much extra spooky magic that scares me.

That's essentially the same reason why @thalesfragoso wanted to keep the MaybeUninit if we removed UnsafeCell: Too much spooky magic so nobody is sure what's actually required.

On the one hand, it's of course wise to not take risks for negligible gains. And even if there was a clear specification that said omitting UnsafeCell is actually fine: If we are the only ones implementing a Mutex without an UnsafeCell, we might also be the first ones to run into obscure compiler bugs...
On the other hand, this feels a bit like cargo-cult programming.

@Dirbaio
Copy link
Member

Dirbaio commented Oct 16, 2024

done in #54

@Dirbaio Dirbaio closed this Oct 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Why UnsafeCell in Mutex?
3 participants