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>需要存储状态(SomeNone某处,因为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;
        }
    }
}

Playground example with tests

正如评论中所建议的,另一种方法是将 OptionNonZeroU32 一起用于 entity_id,并依靠 SomeNone 进行检查是否使用实体。

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 开始。

Playground