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

change Global to use Once #752

Merged
merged 1 commit into from
Jul 14, 2024
Merged

change Global to use Once #752

merged 1 commit into from
Jul 14, 2024

Conversation

bend-n
Copy link
Contributor

@bend-n bend-n commented Jun 11, 2024

improves code quality

@lilizoey
Copy link
Member

lilizoey commented Jun 11, 2024

if you're gonna do this you should at least use the most recent master considering #750

but also you'll need to be certain that this doesn't hurt performance

i dont have time to look at this further rn tho

@bend-n bend-n force-pushed the wheels branch 2 times, most recently from 7871677 to 37df2af Compare June 11, 2024 05:08
@bend-n
Copy link
Contributor Author

bend-n commented Jun 11, 2024

I can run some benchmarks, i guess?

@bend-n
Copy link
Contributor Author

bend-n commented Jun 11, 2024

i ran a benchmark and the performance seems to be about the same.

extern crate test;
#[bench]
fn x(b: &mut test::Bencher) {
    b.iter(|| {
        let x = Global::<String>::default();
        for _ in 0..10 {
            *x.lock() += "304.78";
        }
    })
}

it could probably be a bit faster once get_mut_or_init drops.

@Bromeon Bromeon added quality-of-life No new functionality, but improves ergonomics/internals c: core Core components labels Jun 11, 2024
@Bromeon
Copy link
Member

Bromeon commented Jun 11, 2024

Thanks for the PR!

This uses Mutex<OnceLock<T>>, which requires two synchronizations instead of one. I honestly don't see how this is an improvement over the status quo, at best we get similar/same performance.

If you find concrete bugs with the current implementation, I'd prefer to address those directly and write regression tests for it, but I'm not really willing to introduce extra locking just so we can reuse standard facilities. Yes, I find it mindblowing too that something as basic as ergonomic global variables seems to be pioneering work in Rust 😉

@bend-n
Copy link
Contributor Author

bend-n commented Jun 11, 2024

wait i can swap it to OnceCell

@Bromeon
Copy link
Member

Bromeon commented Jun 11, 2024

Please provide a proper rationale for the change. Your initial message doesn't even have a description.

@bend-n
Copy link
Contributor Author

bend-n commented Jun 11, 2024

the rationale is to reduce the necessary maintenance surface, this makes the code much simpler.
its a refactor.
the old code was rather sketchy, this is much less so.

@Bromeon
Copy link
Member

Bromeon commented Jun 11, 2024

Some smaller disadvantages I see with your approach:

  • The function is still accessible after its use. It might be a bit harder if we e.g. wanted to use FnOnce in the future.
  • The type now occupies space for both the function and the value, while before this was in an enum. It's not a big deal, but it's a problem the current solution doesn't have.

Btw, regarding several of your comments, also on Discord:

can I fix Global

its kinda broken though
like it has a whole useless mutex thats just making api harder

Global: use the wheel, dont reinvent it

the old code was rather sketchy

I could appreciate your changes a lot more without the snarky comments. You may not notice it, but the way you word things comes across as pretty arrogant. I would recommend to familiarize yourself with the existing code and design behind it, before just assuming it's garbage.

Let's keep it technical from now on. Thanks.

@bend-n bend-n changed the title change Global to use OnceLock change Global to use Once Jun 11, 2024
@bend-n
Copy link
Contributor Author

bend-n commented Jun 14, 2024

i believe this change doesn't make FnOnce much harder to swap to? you can just put an Option around it (which keeps the type size the same).
The size of Global before and after remain the same (64).


My bad, will do.

godot-ffi/src/global.rs Outdated Show resolved Hide resolved
Comment on lines 29 to 31
pub struct Global<T, F = fn() -> T> {
// When needed, this could be changed to use RwLock and separate read/write guards.
value: Mutex<InitState<T>>,
value: Mutex<OnceCell<T>>,
init_fn: F,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The F parameter is not necessary; new() in its current form can only be invoked with a function pointer. I'd keep this a pure refactoring without changing the public API, we can discuss new features separately if needed.

Comment on lines 64 to 68
pub fn lock(&self) -> GlobalGuard<'_, T> {
let mutex_guard = self
.value
.lock()
.expect("Global<T> poisoned; a thread has panicked while holding a lock to it");

let guard = Self::ensure_init(mutex_guard, true)
.unwrap_or_else(|| panic!("previous Global<T> initialization failed due to panic"));

guard
let guard = self.value.lock().unwrap_or_else(PoisonError::into_inner);
guard.get_or_init(self.init_fn);
// SAFETY: above line guarantees init
unsafe { GlobalGuard::new_unchecked(guard) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure the get_or_init() isn't more expensive than the previous Self::ensure_init()?

While it may not matter in average cases, it would be a pity to pessimize performance just to make our own life a bit easier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, please also leave an empty line before // comment blocks (here // SAFETY).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the get_or_init here boils down to

if self.get().is_some() { return };
let value = f();
let slot = unsafe { &mut *self.inner.get() };
slot.insert(value);

which seems like an improvement from the previous ensure_init fn.

and ive benchmarked it to be about the same speed.

godot-ffi/src/global.rs Outdated Show resolved Hide resolved
Comment on lines 86 to 88
std::panic::catch_unwind(|| guard.get_or_init(self.init_fn))
.map_err(|_| GlobalLockError::InitFailed)?;
Ok(unsafe { GlobalGuard::new_unchecked(guard) })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious why this is called again here, could you elaborate?

Also, unsafe would need // SAFETY, can be kept short like above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i hope ive clarified it now

Comment on lines 97 to 101
use std::{
cell::OnceCell,
ops::{Deref, DerefMut},
sync::MutexGuard,
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imports

Comment on lines +110 to 122
match mutex_guard.get() {
Some(_) => Some(Self { mutex_guard }),
_ => None,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use map here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you try to avoid just saying "no" like this? it isnt very helpful most of the time and we need to ask follow up questions anyway to understand more clearly. why doesn't map work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it results in an error because it moves the mutex guard into a FnOnce

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error[E0505]: cannot move out of `mutex_guard` because it is borrowed
   --> godot-ffi/src/global.rs:114:35
    |
113 |         pub(super) fn new(mutex_guard: MutexGuard<'a, OnceCell<T>>) -> Option<Self> {
    |                           ----------- binding `mutex_guard` declared here
114 |             mutex_guard.get().map(|_| Self { mutex_guard })
    |             -----------       --- ^^^        ----------- move occurs due to use in closure
    |             |                 |   |
    |             |                 |   move out of `mutex_guard` occurs here
    |             |                 borrow later used by call
    |             borrow of `mutex_guard` occurs here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do this though if you prefer

mutex_guard.get().is_some().then_some(Self { mutex_guard })

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you see what i have right now is basically an eager map

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then let's leave the match. Can you add a comment mentioning that we can't use .get().map(...) due to the guard remaining borrowed?

godot-ffi/src/global.rs Outdated Show resolved Hide resolved
godot-ffi/src/global.rs Outdated Show resolved Hide resolved
godot-ffi/src/global.rs Outdated Show resolved Hide resolved
@bend-n bend-n force-pushed the wheels branch 7 times, most recently from be74d96 to d5c22f9 Compare June 27, 2024 04:24
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-752

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Few things left.

Comment on lines 81 to 84
// SAFETY: `get_or_init()` cant panic, therefore the object is guaranteed to be initialized.
Ok(unsafe { GlobalGuard::new_unchecked(g) })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safety message is imprecise -- get_or_init can panic, but a potential panic is caught above. Reaching this line implies that it didn't panic.

godot-ffi/src/global.rs Outdated Show resolved Hide resolved
Comment on lines 66 to 68
// SAFETY: `get_or_init()` cant panic, therefore the object is guaranteed to be initialized.
unsafe { GlobalGuard::new_unchecked(guard) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, get_or_init can panic but the panic is propagated to the caller.

godot-ffi/src/global.rs Outdated Show resolved Hide resolved
godot-ffi/src/global.rs Outdated Show resolved Hide resolved
Comment on lines +110 to 122
match mutex_guard.get() {
Some(_) => Some(Self { mutex_guard }),
_ => None,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then let's leave the match. Can you add a comment mentioning that we can't use .get().map(...) due to the guard remaining borrowed?

@bend-n bend-n force-pushed the wheels branch 3 times, most recently from 45df6a7 to f1c9301 Compare July 8, 2024 16:59
Copy link
Member

@lilizoey lilizoey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this mostly looks good to me! was only one thing that i noticed.

godot-ffi/src/global.rs Show resolved Hide resolved

/// The value must be initialized.
pub(super) unsafe fn new_unchecked(mutex_guard: MutexGuard<'a, OnceCell<T>>) -> Self {
debug_assert!(mutex_guard.get().is_some(), "safety precondition violated");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"safety precondition violated" isn't very expressive, better would be "cell not initialized" or so.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mind "safety precondition violated: cell not initialized"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, too!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although actually i think unwrap_unchecked already has an assertion so maybe i dont need it at all but it cant hurt anyways

godot-ffi/src/global.rs Outdated Show resolved Hide resolved
fn deref(&self) -> &Self::Target {
// SAFETY: `self` is `Initialized`.
unsafe { self.mutex_guard.as_initialized().unwrap_unchecked() }
// SAFETY: [`GlobalGuard::new`] being the sole constructor guarantees that the cell is initialized.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite accurate; there is another constructor new_unchecked() with the same visibility.

But I think it's enough to mention that the invariant of GlobalGuard is that the cell is initialized. Then ensuring this (and proving its truth via comments) can be done in the constructors.

}
}

impl<T> DerefMut for GlobalGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
// SAFETY: `self` is `Initialized`.
unsafe { self.mutex_guard.as_initialized_mut().unwrap_unchecked() }
// SAFETY: [`GlobalGuard::new`] being the sole constructor guarantees that the cell is initialized.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

@bend-n bend-n force-pushed the wheels branch 3 times, most recently from 5b76701 to d949495 Compare July 13, 2024 11:38
@Bromeon
Copy link
Member

Bromeon commented Jul 13, 2024

The commit message still mentions

Global: changed to wrap Mutex

but you're now using OnceCell. Could you update that? After, the PR should be ready 🙂

@Bromeon Bromeon added this pull request to the merge queue Jul 14, 2024
Merged via the queue into godot-rust:master with commit f0fc6f6 Jul 14, 2024
15 checks passed
@bend-n bend-n deleted the wheels branch July 14, 2024 11:50
@Bromeon
Copy link
Member

Bromeon commented Jul 14, 2024

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Core components quality-of-life No new functionality, but improves ergonomics/internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants