Option 类型是否小于包装类型加上布尔值?
Is an Option type smaller than the wrapped type plus a boolean?
use std::mem::size_of;
struct Position {
x: f32,
y: f32,
z: f32,
}
struct PoolItem {
entity_id: u32, // 4 bytes
used: bool, // 1 bytes + 3 (padding)
component: Position, // 12 bytes
}
assert_eq!(size_of::<u32>(), 4);
assert_eq!(size_of::<Position>(), 12);
assert_eq!(size_of::<PoolItem>(), 20);
如你所见,这样的结构有20个字节长。 Position
实际上是可选的,取决于 used
.
使用 Option
是否会消除对 used
字段的需求并将结构大小减小到 16?
struct PoolItem {
entity_id: u32, // 4 bytes
component: Option<Position>, // 12 bytes ?
}
如果是这样,Option
是如何实现这种行为的?
我在 Playground 上的测试似乎表明它不起作用。为什么?
Option
的精确实现并不重要。显而易见的是,您无法在 X
存储量中存储 X
数据量 并且 也存储数据是否存在。 Option
的一个明显实现是存储对象和指示对象是否存在的布尔值;显然类似的事情正在发生。 Option
是方便,还是得把信息存放在某处。
请注意,在 struct
之外(必须具有一致的大小)Option
可能会避免此成本,如果优化器确定 Option
已知 "populated or not" 状态在任何时候,因此布尔值 可能 被省略以支持代码始终以正确的方式确定性地使用它(如果逻辑上存在,则从堆栈中读取对象,或者不这样做当它没有时)。但在这种情况下,需要额外的数据。
Option<Position>
需要存储状态(Some
或None
)某处,因为Position
已经包含12字节的信息,您需要更多 space 来存储它。通常这意味着它会添加一个额外的字节(加上填充)来存储状态,尽管在某些情况下内部类型具有已知的未使用状态。例如,引用可以指向地址 0
,因此 Option<&'_ T>
可以使用 0
作为 None
状态并占用与 &'_ T
相同的字节数.但是,对于您的 Position
类型,情况并非如此。
如果你绝对需要你的 PoolItem
结构尽可能小,并且你可以从你的 entity_id
字段中腾出一位(比如,最高位,231),你可以用它来存储状态:
const COMPONENT_USED_BIT: u32 = (1u32 << 31);
struct PoolItem {
entity_id: u32, // lowest 31 bits = entity ID, highest bit = "component used"
component: Position,
}
这可能会变得有点复杂,因为您需要确保对该位进行特殊处理,但您可以编写几个简单的访问器方法来确保正确处理该特殊位。
impl PoolItem {
/// Get entity ID, without the "component used" bit
fn entity_id(&self) -> u32 {
self.entity_id & !COMPONENT_USED_BIT
}
/// Set entity ID, keeping the existing "component used" bit
fn set_entity_id(&mut self, entity_id: u32) {
let component_used_bit = self.entity_id & COMPONENT_USED_BIT;
self.entity_id = (entity_id & !COMPONENT_USED_BIT) | component_used_bit;
}
/// Get component if "component used" bit is set
fn component(&self) -> Option<&Position> {
if self.entity_id & COMPONENT_USED_BIT != 0 {
Some(&self.component)
} else {
None
}
}
/// Set component, updating the "component used" bit
fn set_component(&mut self, component: Option<Position>) {
if let Some(component) = component {
self.component = component;
self.entity_id |= COMPONENT_USED_BIT;
} else {
self.entity_id &= !COMPONENT_USED_BIT;
}
}
}
正如评论中所建议的,另一种方法是将 Option
与 NonZeroU32
一起用于 entity_id
,并依靠 Some
和 None
进行检查是否使用实体。
struct PoolItem {
entity_id: Option<core::num::NonZeroU32>, // 4 bytes
component: Position, // 12 bytes
}
fn main() {
assert_eq!(size_of::<u32>(), 4);
assert_eq!(size_of::<Position>(), 12);
assert_eq!(size_of::<PoolItem>(), 16);
}
它使实体 ID 从 1
开始。
use std::mem::size_of;
struct Position {
x: f32,
y: f32,
z: f32,
}
struct PoolItem {
entity_id: u32, // 4 bytes
used: bool, // 1 bytes + 3 (padding)
component: Position, // 12 bytes
}
assert_eq!(size_of::<u32>(), 4);
assert_eq!(size_of::<Position>(), 12);
assert_eq!(size_of::<PoolItem>(), 20);
如你所见,这样的结构有20个字节长。 Position
实际上是可选的,取决于 used
.
使用 Option
是否会消除对 used
字段的需求并将结构大小减小到 16?
struct PoolItem {
entity_id: u32, // 4 bytes
component: Option<Position>, // 12 bytes ?
}
如果是这样,Option
是如何实现这种行为的?
我在 Playground 上的测试似乎表明它不起作用。为什么?
Option
的精确实现并不重要。显而易见的是,您无法在 X
存储量中存储 X
数据量 并且 也存储数据是否存在。 Option
的一个明显实现是存储对象和指示对象是否存在的布尔值;显然类似的事情正在发生。 Option
是方便,还是得把信息存放在某处。
请注意,在 struct
之外(必须具有一致的大小)Option
可能会避免此成本,如果优化器确定 Option
已知 "populated or not" 状态在任何时候,因此布尔值 可能 被省略以支持代码始终以正确的方式确定性地使用它(如果逻辑上存在,则从堆栈中读取对象,或者不这样做当它没有时)。但在这种情况下,需要额外的数据。
Option<Position>
需要存储状态(Some
或None
)某处,因为Position
已经包含12字节的信息,您需要更多 space 来存储它。通常这意味着它会添加一个额外的字节(加上填充)来存储状态,尽管在某些情况下内部类型具有已知的未使用状态。例如,引用可以指向地址 0
,因此 Option<&'_ T>
可以使用 0
作为 None
状态并占用与 &'_ T
相同的字节数.但是,对于您的 Position
类型,情况并非如此。
如果你绝对需要你的 PoolItem
结构尽可能小,并且你可以从你的 entity_id
字段中腾出一位(比如,最高位,231),你可以用它来存储状态:
const COMPONENT_USED_BIT: u32 = (1u32 << 31);
struct PoolItem {
entity_id: u32, // lowest 31 bits = entity ID, highest bit = "component used"
component: Position,
}
这可能会变得有点复杂,因为您需要确保对该位进行特殊处理,但您可以编写几个简单的访问器方法来确保正确处理该特殊位。
impl PoolItem {
/// Get entity ID, without the "component used" bit
fn entity_id(&self) -> u32 {
self.entity_id & !COMPONENT_USED_BIT
}
/// Set entity ID, keeping the existing "component used" bit
fn set_entity_id(&mut self, entity_id: u32) {
let component_used_bit = self.entity_id & COMPONENT_USED_BIT;
self.entity_id = (entity_id & !COMPONENT_USED_BIT) | component_used_bit;
}
/// Get component if "component used" bit is set
fn component(&self) -> Option<&Position> {
if self.entity_id & COMPONENT_USED_BIT != 0 {
Some(&self.component)
} else {
None
}
}
/// Set component, updating the "component used" bit
fn set_component(&mut self, component: Option<Position>) {
if let Some(component) = component {
self.component = component;
self.entity_id |= COMPONENT_USED_BIT;
} else {
self.entity_id &= !COMPONENT_USED_BIT;
}
}
}
正如评论中所建议的,另一种方法是将 Option
与 NonZeroU32
一起用于 entity_id
,并依靠 Some
和 None
进行检查是否使用实体。
struct PoolItem {
entity_id: Option<core::num::NonZeroU32>, // 4 bytes
component: Position, // 12 bytes
}
fn main() {
assert_eq!(size_of::<u32>(), 4);
assert_eq!(size_of::<Position>(), 12);
assert_eq!(size_of::<PoolItem>(), 16);
}
它使实体 ID 从 1
开始。