【问题标题】:Is an Option type smaller than the wrapped type plus a boolean?Option 类型是否小于包装类型加上布尔值?
【发布时间】:2020-07-17 11:09:57
【问题描述】:
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 如何实现这样的行为?

我对@9​​87654322@ 的测试似乎表明它不起作用。为什么?

【问题讨论】:

  • 最初的问题是:“Option 类型可以消除对布尔字段的需要吗?”。编辑后的版本失去了恕我直言的提示。
  • 丢失的“提示”是什么?如果我的编辑违反了问题的精神,您可以随时再次编辑问题;在我看来,这个问题的主要目的是询问使用Option 与使用bool 相比是否会减小PoolItem 的大小(答案是否定的)。无论如何,我的编辑是在前两个答案已经发布之后进行的,所以两个回答者都在回答您的原始问题,而不是我的编辑。

标签: struct rust boolean padding optional


【解决方案1】:

Option 的精确实现并不重要。很明显,您不能将X 的数据量存储在X 的存储量中并且还存储数据是否存在。 Option 的一个明显实现是存储对象和指示对象是否存在的布尔值;显然,类似的事情正在发生。 Option 是一种方便,它仍然需要将信息存储在某个地方。

请注意,在 struct 之外(必须具有一致的大小)Option 可能会避免此成本,如果优化器确定 Option 始终知道“已填充或未填充”状态,因此布尔值 可能被省略,以支持代码始终以正确的方式确定性地使用它(如果它在逻辑上存在,则从堆栈中读取对象,或者在它不存在时不这样做)。但在这种情况下,需要额外的数据。

【讨论】:

  • 由于Position 永远不能是空指针,因此Position 的缺失可以编码为空指针,而Position 对象本身的指针存在。似乎并非如此,但直到现在我都相信Option 是这样编码的,而“零成本抽象”是这种行为的关键字。
  • 没有指针。 Position 的 12 个字节直接嵌入到父结构中。
  • 值得注意的是,将 entity_id 更改为 NonZeroU32 将允许选项布局优化发生,从 id 1 开始的实体成本很小。
  • @CoronA:超出 JohnKugelman 的观点,指针也不会是零成本。您将为指针支付 4-8 个字节,以及最重要的 Position 的全部成本(以及分配器开销)。更糟糕的是,您必须对“其他地方”内存执行指针查找(可能不在缓存行/页面中,因此速度要慢得多),其中嵌入对象直接避免了这种开销(布尔值和对象都是相邻,并且通常应该在同一缓存行中)。避免这种情况意味着将它们分配在一个块中,现在您将指针用作美化的布尔值。
  • @CoronA 值得注意的是,如果您确实将Position 放在不可为空的指针后面,例如Box&lt;Position&gt;&amp;PositionRc&lt;Position&gt;,编译器实际上会编码按照你的建议。 “零成本抽象”是一个通用术语,通常在将内置抽象(如 Option&lt;T&gt;)与修辞的手工编码等效物(如包含 T 和布尔标志的结构)进行比较时使用。从这个意义上说,Option&lt;T&gt; 确实是零成本,因为即使标志是必要的,也无法更好地手动编码。
【解决方案2】:

Option&lt;Position&gt; 需要存储状态(SomeNonesomewhere,因为Position 已经包含 12 个字节的信息,所以需要更多空间来存储它。通常这意味着它会添加一个额外的字节(加上填充)来存储状态,尽管在某些情况下内部类型具有已知的未使用状态。例如,引用可以指向地址0,因此Option&lt;&amp;'_ T&gt; 可以使用0 作为None 状态并占用与&amp;'_ 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

【讨论】:

  • NonZeroU32 呢?
  • @Narann NonZeroU32 和类似类型允许与引用相同的编译器优化(因为0 不是有效状态),所以Option&lt;NonZeroU32&gt; 占用与NonZeroU32 一样多的字节。
  • 当然可以,但是Option&lt;NonZeroU32&gt; 不会避免处理位掩码吗?
  • @Narann 是的,我想它会,但正如问题所写,它询问是否将 component 字段设为可选。使用Option&lt;NonZeroU32&gt; 将使entity_id 字段可选,这是不一样的。正如我所描述的,如果不增加大小(或减小其他东西的大小,如我的回答),就不可能使 Position 类型可选。
  • 谢谢,很遗憾,问题已被编辑并更改了提示。最初的问题是:“Option 类型可以消除对布尔字段的需求吗?”
【解决方案3】:

根据 cmets 中的建议,另一种方法是将 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

【讨论】:

  • 这与 OP 所要求的不同。他们有一个entity_id 和可选的component;你有一个component 和可选的entity_id。不可能使用entity_id 中的利基来编码Position 的存在或不存在。 Frxstrem 的回答显示了如何限制entity_id 的范围以正确执行此操作(但您需要使用整个entity_id,而不仅仅是一个利基值)。
  • 我是 OP,我的原始问题已被编辑。最初的问题是:“选项类型可以消除对布尔字段的需求吗?”。但你在技术上是对的。使用Option&lt;NonZeroU32&gt; 意味着entity_id 在代码库中几乎所有其他地方都是NonZeroU32
  • 是的,我编辑了它,我看不出编辑与这个答案有什么关系。编辑前后的问题都询问如何使用 ID 和可选的Position 制作结构;这个答案显示了如何使用 Position 和可选 ID 创建一个结构。您不能使用此结构来编码值PoolItem { entity_id: 704, component: None },例如,您可以使用Option&lt;Position&gt;。将entity_id 设为可选与将Position 设为可选不同。
猜你喜欢
  • 2017-12-08
  • 1970-01-01
  • 1970-01-01
  • 2012-10-13
  • 2014-03-09
  • 1970-01-01
  • 1970-01-01
  • 2020-01-08
  • 1970-01-01
相关资源
最近更新 更多