C#中布尔值的大小是多少?它真的需要4字节吗



我有两个带有字节数组和布尔值的结构:

using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct struct1
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public byte[] values;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct struct2
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public bool[] values;
}

和以下代码:

class main
{
    public static void Main()
    {
        Console.WriteLine("sizeof array of bytes: "+Marshal.SizeOf(typeof(struct1)));
        Console.WriteLine("sizeof array of bools: " + Marshal.SizeOf(typeof(struct2)));
        Console.ReadKey();
    }
}

这给了我以下输出:

sizeof array of bytes: 3
sizeof array of bools: 12

似乎boolean占用了4个字节的存储空间。理想情况下,boolean只需要一个比特(falsetrue01等)

这里发生了什么?boolean型真的这么低效吗?

bool类型有着复杂的历史,在语言运行时之间有许多不兼容的选择。这始于发明C语言的Dennis Ritchie的历史设计选择。它没有bool类型,可选的是int,其中值0表示false并且任何其他值都被视为true

这个选择是在Winapi中进行的,这是使用pinvoke的主要原因,它有一个BOOL的typedef,这是C编译器的int关键字的别名。如果不应用显式[MarshalAs]属性,则C#bool将转换为bool,从而生成一个4字节长的字段。

无论你做什么,你的结构声明都需要与你在互操作语言中所做的运行时选择相匹配。如前所述,winapi的BOOL,但大多数C++实现都选择了字节,大多数COM Automation互操作使用VARIANT_BOOL,这是的缩写

C#bool实际大小是一个字节。CLR的一个强大的设计目标是你找不到。布局是一个过于依赖处理器的实现细节。处理器对变量类型和对齐非常挑剔,错误的选择会严重影响性能并导致运行时错误。通过使布局不可覆盖,.NET可以提供一个不依赖于实际运行时实现的通用类型系统。

换句话说,您总是必须在运行时封送一个结构来确定布局。此时将进行从内部布局到互操作布局的转换。如果布局相同,这可能非常快,而当字段需要重新排列时,这可能很慢,因为这总是需要创建结构的副本。这方面的技术术语是blittable,将blittable结构传递给本机代码很快,因为pinvokemarshaller可以简单地传递指针。

性能也是bool不是单个位的核心原因。很少有处理器可以直接寻址一个位,最小的单元是一个字节。需要额外的指令从字节中取出位,这不是免费的。而且它从来都不是原子。

C#编译器不会害羞地告诉你它需要1个字节,使用sizeof(bool)。这仍然不是一个预测字段在运行时占用多少字节的好方法,CLR还需要实现.NET内存模型,它承诺简单的变量更新是原子的。这需要变量在内存中正确对齐,这样处理器就可以用一个内存总线周期来更新它。通常情况下,bool实际上需要4或8个字节的内存。为确保下一个成员正确对齐而添加的额外填充。

CLR实际上利用了布局不可忽略的优势,它可以优化类的布局并重新排列字段,从而最大限度地减少填充。比如说,如果你有一个带有bool+int+bool成员的类,那么它将占用1+(3)+4+1+(3)字节的内存,(3)是填充,总共12个字节。50%的浪费。自动布局重新排列为1+1+(2)+4=8字节。只有类具有自动布局,结构默认具有顺序布局。

更糟糕的是,在使用支持AVX指令集的现代C++编译器编译的C++程序中,bool可能需要多达32个字节。这就要求了32字节的对齐,bool变量最终可能会有31字节的填充。另外,.NET抖动不发出SIMD指令的核心原因是,除非显式包装,否则它无法获得对齐保证。

首先,这只是interop的大小。它并不表示数组托管代码中的大小。这是每个bool 1个字节——至少在我的机器上是这样。你可以用这个代码为自己测试:

using System;
class Program 
{ 
    static void Main(string[] args) 
    { 
        int size = 10000000;
        object array = null;
        long before = GC.GetTotalMemory(true); 
        array = new bool[size];
        long after = GC.GetTotalMemory(true); 
        double diff = after - before; 
        Console.WriteLine("Per value: " + diff / size);
        // Stop the GC from messing up our measurements 
        GC.KeepAlive(array); 
    } 
}

现在,对于按值编组数组,正如您所做的那样,文档中写道:

当MarshalAsAttribute.Value属性设置为ByValArray时,必须设置SizeConst字段以指示数组中的元素数。当需要区分字符串类型时,ArraySubType字段可以可选地包含数组元素的UnmanagedType。只能在元素在结构中显示为字段的数组上使用此UnmanagedType

所以我们看一下ArraySubType,它有以下文档:

可以将此参数设置为UnmanagedType枚举中的值,以指定数组元素的类型。如果未指定类型,则使用与托管数组的元素类型相对应的默认非托管类型。

现在看看UnmanagedType,有:

布尔
一个4字节的布尔值(true!=0,false=0)。这是Win32 BOOL类型。

因此,这是bool的默认值,它是4个字节,因为它对应于Win32 BOOL类型——因此,如果您正在与期望BOOL数组的代码进行互操作,那么它正是您想要的。

现在,您可以将ArraySubType指定为I1,它被记录为:

一个1字节的带符号整数。您可以使用此成员将布尔值转换为1字节的C样式布尔(true=1,false=0)。

因此,如果您正在交互操作的代码期望每个值有1个字节,只需使用:

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.I1)]
public bool[] values;

然后,您的代码将显示为每个值占用1个字节,正如预期的那样。

其他答案显然是正确的,布尔是1字节。这添加了一个工作示例,表明bool确实只读取和写入一个字节的内存,不多也不少。

using System;
using System.Runtime.InteropServices;
public class Program
{
    [StructLayout(LayoutKind.Explicit)]
    struct BoolIntUnion
    {
        [FieldOffset(0)]
        public UInt32 i;
        [FieldOffset(0)]
        public bool b;
    }
    public static void Main()
    {
        var u = new BoolIntUnion();
        //first let's see how many bits a boolean reads from memory
        //we will do this by reading/writing an Int32 and a boolean to the same place in memory and observe the results
        //if a bool is only 8 bits, then only the first 8 bits of a UInt32 will make the boolean become true
        u.i = 0b00000000_00000000_00000000_00000001; //try bit 1
        if (u.b) Console.WriteLine("True for " + u.i);
        u.i = 0b00000000_00000000_00000000_10000000; //try bit 8
        if (u.b) Console.WriteLine("True for " + u.i);
        //now set all bits on except for the first 8, the boolean should be false if it only accesses the first 8 bits
        u.i = 0b11111111_11111111_11111111_00000000;
        if (!u.b) Console.WriteLine("False for " + u.i);
        //now let's go the other way and see how many bits a boolean writes to memory
        u.i = 0b11111111_11111111_11111111_11111111;
        u.b = false; //overlay a boolean "false" on top of an UInt32 that has all the bits turned on
        if (u.i == 0b11111111_11111111_11111111_00000000) Console.WriteLine("Overlaying a bool on top of a UInt32 cleared only the first 8 bits");
    }
}

最新更新