一个字节的布尔值。为什么?

44

在C++中,为什么bool类型需要一个字节的存储空间来存储true或false,而一个bit就足够了,比如用0表示false,1表示true?(Java也为什么需要一个字节?)

另外,使用以下代码有多安全?

struct Bool {
    bool trueOrFalse : 1;
};

第三,即使它是安全的,上述领域技术真的会有所帮助吗?因为我听说我们在那里节省空间,但仍然编译器生成的访问它们的代码比访问基本类型生成的代码更大、更慢。


23
除非你有数十亿个字节,否则在2013年担心个别字节是没有意义的。 - Almo
39
最小可寻址的存储单元是字节(Byte),这就是布尔值使用整个字节的原因。 - fge
5
使用字节更快且更易于寻址。 - dchhetri
5
请注意,即使是空结构体,其大小也为1字节。 - leemes
8
除了其他一切,使用1位布尔值进行原子交错操作并不合理。修改位需要昂贵的内核锁来更改单个布尔状态。 - Martin James
显示剩余13条评论
5个回答

92

为什么bool需要一个字节来存储true或false,而一个位就足够了?

因为C++中的每个对象都必须是可寻址的*(也就是说,您必须能够有指向它的指针)。您无法访问单个位(至少不是在传统硬件上)。

使用以下内容有多安全?

它是“安全”的,但并没有实现太多。

上述字段技术真的会有所帮助吗?

不会,原因与上述相同;)

但是,编译器生成的代码访问它们比访问原语的代码更大且更慢。

是的,这是真的。在大多数平台上,这需要访问包含的字节(或int或其他内容),然后执行位移和位掩码操作以访问相关位。


如果您真的关心内存使用情况,您可以在C++中使用std::bitset或在Java中使用BitSet,它们可以打包位。

* 除了少数例外。


3
我们C++开发者应该更频繁地链接Java! - GManNickG
19
你还记得32位系统的4GB内存限制吗?现在请注意,32位系统的最小可寻址单位是一个比特,造成了仅支持500MB内存的限制。 - s3rius
2
@Thomas 它看起来像 Intel 8051 - DJClayworth
3
在C ++中,并非每个对象都必须是可寻址的。例如,位字段不可寻址,而类的第一个元素与包含它的类具有相同的地址,因此它是可寻址但不是单独可寻址的。 - K-ballo
5
是的,它们是这样的。请见1.8/5:“除非它是位域(9.6),否则最终派生对象必须具有非零大小,并且必须占用一个或多个字节的存储空间。” - K-ballo
显示剩余3条评论

11

使用单个比特位速度慢且分配更加复杂。在C/C++中,无法获取一个比特位的地址,因此您无法将&trueOrFalse用作比特位。

Java有BitSet和EnumSet两种使用位图的方法。如果您只有很少的数字,可能不会有太大区别。例如,对象必须至少按字节对齐,在HotSpot中是8字节对齐(在C++中,new Object可以是8到16字节对齐)。这意味着节省几个比特位可能并不会节省任何空间。

至少在Java中,位操作并不比其他操作更快,除非它们更适合缓存。

public static void main(String... ignored) {
    BitSet bits = new BitSet(4000);
    byte[] bytes = new byte[4000];
    short[] shorts = new short[4000];
    int[] ints = new int[4000];

    for (int i = 0; i < 100; i++) {
        long bitTime = timeFlip(bits) + timeFlip(bits);
        long bytesTime = timeFlip(bytes) + timeFlip(bytes);
        long shortsTime = timeFlip(shorts) + timeFlip(shorts);
        long intsTime = timeFlip(ints) + timeFlip(ints);
        System.out.printf("Flip time bits %.1f ns, bytes %.1f, shorts %.1f, ints %.1f%n",
                bitTime / 2.0 / bits.size(), bytesTime / 2.0 / bytes.length,
                shortsTime / 2.0 / shorts.length, intsTime / 2.0 / ints.length);
    }
}

private static long timeFlip(BitSet bits) {
    long start = System.nanoTime();
    for (int i = 0, len = bits.size(); i < len; i++)
        bits.flip(i);
    return System.nanoTime() - start;
}

private static long timeFlip(short[] shorts) {
    long start = System.nanoTime();
    for (int i = 0, len = shorts.length; i < len; i++)
        shorts[i] ^= 1;
    return System.nanoTime() - start;
}

private static long timeFlip(byte[] bytes) {
    long start = System.nanoTime();
    for (int i = 0, len = bytes.length; i < len; i++)
        bytes[i] ^= 1;
    return System.nanoTime() - start;
}

private static long timeFlip(int[] ints) {
    long start = System.nanoTime();
    for (int i = 0, len = ints.length; i < len; i++)
        ints[i] ^= 1;
    return System.nanoTime() - start;
}

打印

Flip time bits 5.0 ns, bytes 0.6, shorts 0.6, ints 0.6

对于大小为40000和400K的尺寸

Flip time bits 6.2 ns, bytes 0.7, shorts 0.8, ints 1.1

适用于 4M

Flip time bits 4.1 ns, bytes 0.5, shorts 1.0, ints 2.3

和40M

Flip time bits 6.2 ns, bytes 0.7, shorts 1.1, ints 2.4

1
不确定速度问题是否如此清晰。例如,vector<bool> 进行位压缩,通常比使用 vector<char> 存储 0 或 1 要快得多。 - user515430
4
据我所知,只有当比特适合缓存而字符不适合时,才会更快。进行比特打包/解包需要额外的工作,而字符则不需要。 - Peter Lawrey
1
你忽略了与内存相关的数量级顺序的另一端发生的情况。如果你的vector<bool>适合RAM或生成较少的页面错误,那么你会获得很大的优势。尝试实现Jon Bentley在Programming Pearls Column 1中对800个数字进行排序的方法。 - user515430
使用Java时,您会遇到集合和数组不能超过20亿的问题。以位为单位的话,这只相当于256 MB,在今天来说,这真是微不足道。 - Peter Lawrey

6
如果您想要存储一个位的信息,那么没有比 char 更紧凑的了,它是 C/C++ 中最小的可寻址内存单元。(根据实现方式,bool 可能与 char 大小相同,但 允许更大。)
C 标准保证 char 至少可以容纳 8 个位,但也可以包含更多。确切的数量可以通过在 limits.h(在 C 中)或 climits(在 C++ 中)中定义的 CHAR_BIT 宏获得。今天,最常见的是 CHAR_BIT == 8,但您不能依赖它(请参见 这里)。但是,在 POSIX 兼容系统和 Windows 上,保证为 8。

虽然无法减少单个标志的内存占用,但当然可以组合多个标志。除了手动执行所有位操作外,还有一些替代方案:

  • 如果在编译时知道位数
    1. 位域(与您的问题中相同)。但请注意,字段的顺序不能保证,这可能会导致可移植性问题。
    2. std::bitset
  • 如果仅在运行时知道大小
    1. boost::dynamic_bitset
    2. 如果要处理大型位向量,请查看BitMagic库。它支持压缩并经过高度调整。
正如其他人已经指出的那样,仅仅为了节省一些位并不总是一个好主意。可能的缺点包括:
  1. 代码可读性降低
  2. 由于额外的提取代码,执行速度降低。
  3. 出于同样的原因,代码大小增加,这可能会抵消数据消耗的节省。
  4. 多线程程序中存在隐藏的同步问题。例如,两个不同线程通过两个不同的位翻转可能会导致竞态条件。相比之下,两个线程修改两个基本类型(例如char)的不同对象总是安全的。
通常,只有在处理大量数据时才有意义,因为这样可以减少内存和缓存的压力。

1
char 是 C/C++ 保证提供的最小类型。一些编译器可能提供更小的类型,有或没有限制。我记得有一种用于图形设计的芯片,其中所有地址都是位地址,因此将 char* 增加一个值需要将指针所表示的值加上 8。从未对齐的地址读取 char 会比从对齐的地址读取速度慢,但不需要任何额外的指令。此外,许多较小的微控制器具有高效的位测试/设置/清除指令,... - supercat
针对这些微控制器的编译器通常提供了有效的使用方式,尽管编译器通常无法通过指针访问这些内容。 - supercat

5

为什么不将状态存储为字节?没有实际测试下面的代码,但是应该能给你一个想法。你甚至可以利用短整型或整型来表示16或32个状态。我相信我有一个可工作的JAVA示例。我找到它后会发布。

__int8 state = 0x0;

bool getState(int bit)
{
  return (state & (1 << bit)) != 0x0;
}

void setAllOnline(bool online)
{
  state = -online;
}

void reverseState(int bit)
{
   state ^= (1 << bit);
}

好的,这是JAVA版本。我将其存储为Int值,因为如果我记得正确,即使使用byte也会使用4个字节。而且这显然不会被用作数组。

public class State
{
    private int STATE;

    public State() { 
        STATE = 0x0;
    }

    public State(int previous) { 
        STATE = previous;
    }


   /*
    * @Usage - Used along side the #setMultiple(int, boolean);
    * @Returns the value of a single bit.
    */
    public static int valueOf(int bit)
    {
        return 1 << bit;
    }


   /*
    * @Usage - Used along side the #setMultiple(int, boolean);
    * @Returns the value of an array of bits.
    */
    public static int valueOf(int... bits)
    {
        int value = 0x0;
        for (int bit : bits)
            value |= (1 << bit);
        return value;
    }


   /*
    * @Returns the value currently stored or the values of all 32 bits.
    */
    public int getValue() 
    {
        return STATE;
    }

   /*
    * @Usage - Turns all bits online or offline.
    * @Return - <TRUE> if all states are online. Otherwise <FALSE>.
    */
    public boolean setAll(boolean online)
    {
        STATE = online ? -1 : 0;
        return online;
    }


   /*
    * @Usage - sets multiple bits at once to a specific state.
    * @Warning - DO NOT SET BITS TO THIS! Use setMultiple(State.valueOf(#), boolean);
    * @Return - <TRUE> if states were set to online. Otherwise <FALSE>.
    */
    public boolean setMultiple(int value, boolean online)
    {
        STATE |= value;
        if (!online)
            STATE ^= value;
        return online;
    }


   /*
    * @Usage - sets a single bit to a specific state.
    * @Return - <TRUE> if this bit was set to online. Otherwise <FALSE>.
    */
    public boolean set(int bit, boolean online)
    {
        STATE |= (1 << bit);
        if(!online)
            STATE ^= (1 << bit);
        return online;
    }


   /*
    * @return = the new current state of this bit.
    * @Usage = Good for situations that are reversed.
    */
    public boolean reverse(int bit)
    {
        return (STATE ^= (1 << bit)) == (1 << bit);
    }


   /*
    * @return = <TRUE> if this bit is online. Otherwise <FALSE>.
    */
    public boolean online(int bit)
    {
        int value = 1 << bit;
        return (STATE & value) == value;        
    }


   /*
    * @return = a String contains full debug information.
    */
    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append("TOTAL VALUE: ");
        sb.append(STATE);
        for (int i = 0; i < 0x20; i++)
        {
            sb.append("\nState(");
            sb.append(i);
            sb.append("): ");
            sb.append(online(i));
            sb.append(", ValueOf: ");
            sb.append(State.valueOf(i));
        }
        return sb.toString();
    }
}

还有一点需要指出的是,你真的不应该为此使用特殊类,而是将变量存储在最可能使用它的类中。如果你计划拥有数百甚至数千个布尔值,请考虑使用字节数组。

例如下面的示例:

boolean[] states = new boolean[4096];

可以转换为以下内容。
int[] states = new int[128];

现在你可能想知道如何从一个128数组中访问索引4095。简化一下,这个操作会将4095向右移动5位,相当于除以32。所以4095/32=向下取整(127),我们现在位于数组的第127个位置。然后我们执行4095&31,它将被转换为0到31之间的值。这只适用于2的幂减1,例如0、1、3、7、15、31、63、127、255、511、1023等。
现在我们可以访问该位置的位。可以看出,这种方法非常紧凑,比在文件中使用4096个布尔值要好得多 : ) 这还将提供更快的二进制文件读/写速度。我不知道这个BitSet是什么,但它看起来像是垃圾,而且由于byte、short、int、long已经以它们的位形式存在,你可以直接使用它们。然后创建一些复杂的类来从内存中访问各个位,这是我从阅读几篇文章中理解到的。
boolean getState(int index)
{
    return (states[index >> 5] & 1 << (index & 0x1F)) != 0x0;
}

更多信息...

基本上,如果上面的内容有点混乱,这里是一个简化版本。

类型"byte"、"short"、"int"、"long"都是具有不同范围的数据类型。

您可以查看此链接:http://msdn.microsoft.com/en-us/library/s3f49ktz(v=vs.80).aspx,以查看每种类型的数据范围。

因此,一个字节等于8位。所以一个4字节的int将是32位。

现在没有任何简单的方法来执行某个值的N次幂。但是,由于位移,我们可以模拟它。通过执行1 << N,这相当于1 * 2^N。因此,如果我们做2 << 2^N,我们将做2 * 2^N。因此,要执行二的幂,请始终执行“1 << N”。

现在我们知道int将有32位,因此可以使用每个位,因此我们可以简单地对它们进行索引。

为了保持简单,将"&"运算符视为检查一个值是否包含另一个值的位的方法。因此,假设我们有一个值为31。要达到31。我们必须添加以下位0到4。它们是1、2、4、8和16。这些加起来等于31。现在,当我们执行31 & 16时,这将返回16,因为位4即2^4 = 16。位于该值中。现在假设我们执行31 & 20,这是检查位2和4是否位于该值中。这将返回20,因为位2和4都位于此处2^2 = 4 + 2^4 = 16 = 20。现在假设我们执行31 & 48。这是检查位4和5。好吧,我们在31中没有第5位。因此,这只会返回16。它不会返回0。因此,在执行多个检查时,必须检查它是否等于该值。而不是检查它是否等于0。

以下内容将验证单个位是否为0或1。0表示false,1表示true。

bool getState(int bit)
{
    return (state & (1 << bit)) != 0x0;
}

以下是检查两个值是否包含这些位的示例。可以将每个位表示为2^BIT,因此当我们进行以下运算时:
我会快速介绍一些操作符。我们刚刚稍微解释了"&"操作符。现在是"|"操作符。
执行以下操作时:
int value = 31;
value |= 16;
value |= 16;
value |= 16;
value |= 16;

该值仍将为31。这是因为位4或2 ^ 4 = 16已经被打开或设置为1。因此执行“|”返回具有该位已打开的该值。如果它已经打开,就不会进行任何更改。我们使用“| =”实际上将变量设置为返回值。
与其执行“value = value | 16;”不如直接执行“value |= 16;”。
现在让我们进一步了解“&”和“|”如何被利用。
/*
 * This contains bits 0,1,2,3,4,8,9 turned on.
 */
const int CHECK = 1 | 2 | 4 | 8 | 16 | 256 | 512;

/*
 * This is some value were we add bits 0 through 9, but we skip 0 and 8.
 */
int value = 2 | 4 | 8 | 16 | 32 | 64 | 128 | 512;

当我们执行以下代码时。
int return_code = value & CHECK;

返回代码将为2 + 4 + 8 + 16 + 512 = 542。

所以我们正在检查799,但是我们收到了542。这是因为位o和8离线,我们等于256 + 1 = 257,799 - 257 = 542。

以上是检查是否按下某些按钮的绝佳方法,例如我们要制作一个视频游戏,并希望检查是否按下了某些按钮。我们可以使用一次检查来检查每个位,这比在每个状态上执行布尔检查更有效率。

现在假设我们有一个始终反转的布尔值。

通常你会做这样的事情:

bool state = false;

state = !state;

使用位运算符"^"也可以实现此操作。

就像我们执行"1 << N"来选择该位的整个值一样,我们也可以反过来做同样的事情。所以就像我们展示了"|="存储返回值一样,我们将使用"^="做同样的事情。所以这个操作的作用是,如果该位为开启状态,则关闭它;如果该位为关闭状态,则打开它。

void reverseState(int bit)
{
   state ^= (1 << bit);
}

您甚至可以让它返回当前状态。如果您希望它返回先前的状态,只需将 "!=" 替换为 "== "。因此,这样做会执行反转,然后检查当前状态。

bool reverseAndGet(int bit)
{
    return ((state ^= (1 << bit)) & (1 << bit)) != 0x0;
}

将多个非单一位的布尔值存储到一个整数中也是可行的。比如我们通常写出的坐标位置如下所示。
int posX = 0;
int posY = 0;
int posZ = 0;

现在假设这些数字从未超过1023。因此,0到1023是所有数字的最大距离。我选择1023是为了其他目的,如前所述,你可以操纵“&”变量作为一种强制值在0和2^N-1之间的方法。所以假设你的范围是0到1023。我们可以执行“value & 1023”,它将始终是一个介于0和1023之间的值,而无需进行任何索引参数检查。请记住,如前所述,这仅适用于2的幂减1。2^10 = 1024 - 1 = 1023。
例如,不再需要if(value>=0&& value<=1023)。
因此,2^10 = 1024,需要10位才能容纳0到1023之间的数字。
因此,10x3 = 30仍然小于或等于32。对于int类型来说,足以容纳所有这些值。
因此,我们可以执行以下操作。要查看使用了多少位,请执行0 + 10 + 20。我在那里放置0的原因是向您展示可视化效果,即2^0 = 1,因此# * 1 = #。我们需要y << 10的原因是x使用了10位,即0到1023。因此,我们需要将y乘以1024以获得每个值的唯一值。然后,Z需要乘以2^20,即1,048,576。
int position = (x << 0) | (y << 10) | (z << 20);

这使得比较变得更快速。
我们现在可以进行:
return this.position == position;

与之相对的是
return this.x == x && this.y == y && this.z == z;

现在,如果我们想要每个位置的实际坐标,该怎么办呢?
对于x,我们只需执行以下操作。
int getX()
{ 
   return position & 1023;
}

对于 y,我们需要执行左位移,然后进行 AND 操作。

int getY()
{ 
   return (position >> 10) & 1023;
}

你可能会猜到Z和Y是一样的,但我们使用的数字是20而不是10。

int getZ()
{ 
   return (position >> 20) & 1023;
}

我希望阅读此文的人能够获得有价值的信息 :).

+1非常有价值的介绍,关于如何处理基本类型的位运算 :) - lekroif
谢谢,我已经包含了额外的信息,并提供了一些例子。这样任何人来到这里都可以真正了解位的惊人用途。我从未使用过这个叫做“BitSet”的东西,但看着它的Java版本,似乎完全没用。我很惊讶这里很少有评论谈论位移。我甚至不太了解它,但我知道足够的知识来利用它所提供的好功能。 - Jeremy Trifilo

4
如果你真的想使用1位,你可以使用一个char类型来存储8个布尔值,并进行位移操作以获取所需的值。我怀疑这种方法不会更快,并且可能会给你带来许多麻烦,但从技术上讲是可行的。
另外,像这样的尝试对于那些没有足够内存用于变量但有比你需要的更多的处理能力的系统可能会很有用。尽管如此,我非常怀疑你会真正需要它。

好的...我也这么想 :) 谢谢! - Sam
我曾使用一种特定的软件进行编程,其中唯一类似变量的东西是事件,基本上就是一个布尔值。我试图在我的应用程序中实现一个评分系统,并使用了8个事件来模拟一个字符,通过开关它们来实现。这就是为什么现在想起它,它让我想起了那个地狱般的经历 xD。 - Kevin
1
在 ANSI C 中,char 不一定是 8 位。请参见 limits.h 中的 CHAR_BIT - Michał Šrajer
1
@MichałŠrajer 在Java中,char是16位的 :) - fredoverflow
在C++中,std::vector<bool>使用了这种技术进行特化,但是实现起来并不是很有用,而且速度也很慢。我不确定,但这种特化可能仍在讨论中是否应该被移除。 - dchhetri
1
@user814628 有计划要么删除特化,要么保留但弃用vector<bool>的使用。看起来,在C++11中都没有执行。我不知道未来的计划是什么。来源(关于vector<bool>的Boost):http://www.boost.org/doc/libs/1_52_0/doc/html/container/Cpp11_conformance.html#container.Cpp11_conformance.Vector_bool - Philipp Claßen

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