为什么Java中的布尔值至少要有1个字节大小?

6
众所周知,C++中的bool类型必须至少为1个字节大小,以便为每个指针创建指针,详情请见[https://dev59.com/LHI95IYBdhLWcg3w_zMN#2064565]。但是在Java中,原始类型没有指针。不过,它们仍然占用至少一个字节,请见[https://dev59.com/F3RC5IYBdhLWcg3wMd9S#383597]
那么为什么Java的布尔类型不能为1位呢?除了计算时间外,如果有一个很大的布尔数组,我们肯定可以构思一个编译器,进行适当的位移来检索对应于布尔值的各个位。

2
@Mango 嗯...但这引出了一个问题,正如我在文本中所暗示的:为什么必须首先寻址boolean?难道不能8个booleans(例如在boolean数组中)共享同一个地址,然后根据数组索引适当地进行移位吗? - flow2k
2
强制使用紧凑表示法只会在空间不是问题时减慢速度。如果需要空间,您可以自己打包或使用库实现。 - user2357112
1
可能是为什么布尔类型占用1字节而不是1位?的重复问题。 - Oleksandr Pyrohov
3
是的,它们可以。 - Silvio Mayolo
2
这个问题断言他们“不能”,但我不知道这是真的。它在哪里说他们不能呢?如果你问为什么它们不在任何现有的JVM中(这很可能),那么“除去计算时间”就没有多大意义了;计算时间是进行任何优化的原因,就像这个问题一样。 - yshavit
显示剩余5条评论
1个回答

12
没有理由一个布尔值一定要占用一个字节的空间。实际上,在某些情况下,布尔值可能已经不是1字节(有效)大小了:当它们与堆栈或对象中大于1字节的其他元素紧密排列时,它们很可能会变得更大(即,将它们添加到对象可能会导致大小增加超过一个字节)。
任何JVM都可以将布尔值实现为1位,但据我所知,没有选择这样做的,可能主要是因为:
  • 访问一位通常比访问一个字节更昂贵,特别是在写入时。
阅读一点,使用“经典RISC”指令集的CPU通常需要额外的“and”指令来从一个打包的字节(或更大的字)中提取相关位。有些甚至需要额外的指令来加载一个常量到“and”。在索引一个布尔数组的情况下,其中位索引在编译时不固定,你需要一个可变的位移。一些CPU(如x86)更容易,因为它们具有内存源“test”指令,包括采用可变位置的特定位测试指令,如“bt”。这样的CPU可能在两种表示中具有类似的读取性能。
写入更糟糕:不再是简单的字节写入来设置布尔值,现在需要读取值,修改适当的位并将其写回。一些平台(如x86)具有内存源和目的地的RMW指令,如“and”和“or”,这将有所帮助,但这些指令仍然比普通写入要昂贵得多。在最坏的情况下,重复写入相同的元素将导致通过内存的依赖链,这可能使您的代码减慢一个数量级(一系列普通存储无法形成依赖链)。
更糟糕的是,上述的写入方法完全不安全。两个线程同时操作“独立”的布尔值可能会相互干扰,因此运行时必须使用原子更新操作来写入任何字段的位,以确保对象对于线程是本地的无法被证明。
通常情况下,除了数组之外的空间节省非常小,甚至可能为零:对齐问题意味着一个单独的位通常会占用与堆栈或对象布局中的一个字节相同的空间。只有当您在堆栈或对象上有许多原始的布尔值时,您才会看到节省空间的效果(例如,对象通常对齐到8字节边界,因此如果您有一个非布尔字段为int或更大的对象,您至少需要4个布尔值才能节省任何空间,而通常您需要8个)。
这使得位表示的布尔值在布尔数组中成为最后剩下的“大赢家”,在大型数组中,您可以节省8倍的空间。事实上,在C++世界中,这种情况足够引人注目,以至于那里的vector有一个“特殊”的实现,其中每个bool占用一个位 - 这是一个永无止境的头痛之源,因为所有所需的特殊情况和非直观行为(并且通常被用作无法删除的错误功能的示例)。
如果没有内存模型,我可以想象一个世界,在这个世界中,Java以位方式实现布尔数组。它们没有与vector相同的问题(主要是因为JIT提供的额外抽象层以及数组提供比vector更简单的接口),并且可以高效地完成。然而,存在一个麻烦的内存模型。该模型允许不同线程对不同数组元素的写入是安全的(即,它们在内存模型的目的下作为独立变量)。所有常见的CPU都直接支持这一点,如果将布尔实现为字节,则它们具有独立的字节访问。然而,没有CPU提供独立的位访问:您只能使用原子操作(x86提供了lock bt*操作,但速度较慢;其他平台的选项更糟糕)。这将破坏任何以位数组实现的布尔数组的性能。
最后,如上所述,将布尔实现为位具有显著的缺点-但是好处呢?
事实证明,如果用户真的想要这种位压缩的布尔表示,他们可以自己实现!他们可以将8个布尔值打包到一个字节(或32个值到一个整数或其他)的对象中(这在标志等情况下很常见),生成的访问器代码应该和JVM本地支持布尔作为位一样高效。实际上,当你确定你想要一个大量布尔值的位数组表示时,你可以简单地使用BitSet - 它具有你想要的表示,并通过不提供任何线程安全保证来避免原子问题。因此,通过将布尔实现为字节,您可以避开上述所有问题,但仍然让用户在不会有太多运行时开销的情况下选择位级表示。

1
我认为,“非常大”应该改为“非常小”。此外,我会使用“原子更新”而不是“原子操作”这个术语,因为后者范围更广,包括不那么昂贵的普通读写操作。 - Holger
@Holger - 是的,很好发现这个问题,应该是“非常小”。我仍然保留了原子操作,因为我认为从上下文中可以清楚地看出我们谈论的是硬件级别的原子/锁定操作,而不是在某些平台上偶然具有原子性的读取或写入。这在这个领域中是惯用术语。无论如何,原子更新并没有太大帮助,因为写入是更新但不一定昂贵,对吧?也许可以说“原子RMW”,但这甚至更加晦涩,有些特定于平台。 - BeeOnRope
1
即使是“原子更新”,如果我们不应用其他线程可见的所有更新语义,那么它也不必非常昂贵,但当然,对于这样一个低级操作,每个周期都很重要... - Holger
3
@flow2k - 正确。boolean 本身仍然是一个字节(并且作为一个字节访问),但由于对象被舍入到下一个对齐边界,因此将 boolean 添加到对象中通常具有与添加 int 相同的效果。例如,Java 对象通常在 64 位平台上具有 8 字节对齐,因此如果您取一个恰好为 16 字节的对象并添加一个单独的 boolean,它将变为 24 字节。 - BeeOnRope
2
8字节对齐适用于大多数32位JVM。 - undefined
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接