Split GenericTypeCell::get_or_insert into smaller pieces (#14865)

# Objective

Based on the discussion in #14864, I wanted to experiment with the core
`GenericTypeCell` type, whose `get_or_insert` method accounted for 2% of
the final binary size of the `3d_scene` example. The reason for this
large percentage is likely because the type is fundamental to the rest
of Bevy while having 3 generic parameters (the type stored `T`, the type
to retrieve `G`, and the function used to insert a new value `F`).

- Acts on #14864 

## Solution

- Split `get_or_insert` into smaller functions with minimised
parameterisation. These new functions are private as to preserve the
public facing API, but could be exposed if desired.

## Testing

- Ran CI locally.
- Used `cargo bloat --release --example 3d_scene -n 100000
--message-format json > out.json` and @cart's [bloat
analyzer](https://gist.github.com/cart/722756ba3da0e983d207633e0a48a8ab)
to measure a 428KiB reduction in binary size when compiling on Windows
10.
- ~I have _not_ benchmarked to determine if this improves/hurts
performance.~ See
[below](https://github.com/bevyengine/bevy/pull/14865#issuecomment-2306083606).

## Notes

In my opinion this seems like a good test-case for the concept of
debloating generics within the Bevy codebase. I believe the performance
impact here is negligible in either direction (at runtime and compile
time), but the binary reduction is measurable and quite significant for
a relatively minor change in code.

---------

Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
This commit is contained in:
Zachary Harrold 2024-08-27 04:20:01 +10:00 committed by GitHub
parent 96f1fd73cb
commit 44620dd6ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -239,22 +239,38 @@ impl<T: TypedProperty> GenericTypeCell<T> {
G: Any + ?Sized,
F: FnOnce() -> T::Stored,
{
let type_id = TypeId::of::<G>();
self.get_or_insert_by_type_id(TypeId::of::<G>(), f)
}
// Put in a separate scope, so `mapping` is dropped before `f`,
// since `f` might want to call `get_or_insert` recursively
// and we don't want a deadlock!
{
let mapping = self.0.read().unwrap_or_else(PoisonError::into_inner);
if let Some(info) = mapping.get(&type_id) {
return info;
}
/// Returns a reference to the [`TypedProperty`] stored in the cell, if any.
///
/// This method will then return the correct [`TypedProperty`] reference for the given type `T`.
fn get_by_type_id(&self, type_id: TypeId) -> Option<&T::Stored> {
self.0
.read()
.unwrap_or_else(PoisonError::into_inner)
.get(&type_id)
.copied()
}
/// Returns a reference to the [`TypedProperty`] stored in the cell.
///
/// This method will then return the correct [`TypedProperty`] reference for the given type `T`.
/// If there is no entry found, a new one will be generated from the given function.
fn get_or_insert_by_type_id<F>(&self, type_id: TypeId, f: F) -> &T::Stored
where
F: FnOnce() -> T::Stored,
{
match self.get_by_type_id(type_id) {
Some(info) => info,
None => self.insert_by_type_id(type_id, f()),
}
}
let value = f();
let mut mapping = self.0.write().unwrap_or_else(PoisonError::into_inner);
mapping
fn insert_by_type_id(&self, type_id: TypeId, value: T::Stored) -> &T::Stored {
self.0
.write()
.unwrap_or_else(PoisonError::into_inner)
.entry(type_id)
.insert({
// We leak here in order to obtain a `&'static` reference.