【问题标题】:Why does an empty struct in C# consume memory为什么 C# 中的空结构会消耗内存
【发布时间】:2016-03-20 05:33:41
【问题描述】:

我一直理解结构(值类型)包含的字节数与结构字段中定义的字节数完全相同……但是,我做了一些测试,空结构似乎有一个例外:

public class EmptyStructTest
{
    static void Main(string[] args)
    {
        FindMemoryLoad<FooStruct>((id) => new FooStruct());
        FindMemoryLoad<Bar<FooStruct>>((id) => new Bar<FooStruct>(id));
        FindMemoryLoad<Bar<int>>((id) => new Bar<int>(id));
        FindMemoryLoad<int>((id) => id);
        Console.ReadLine();
    }

    private static void FindMemoryLoad<T>(Func<int, T> creator) where T : new()
    {
        GC.Collect(GC.MaxGeneration);
        GC.WaitForFullGCComplete();
        Thread.MemoryBarrier();
        long start = GC.GetTotalMemory(true);

        T[] ids = new T[10000];
        for (int i = 0; i < ids.Length; ++i)
        {
            ids[i] = creator(i);
        }

        long end = GC.GetTotalMemory(true);
        GC.Collect(GC.MaxGeneration);
        GC.WaitForFullGCComplete();
        Thread.MemoryBarrier();

        Console.WriteLine("{0} {1}", ((double)end-start) / 10000.0, ids.Length);
    }

    public struct FooStruct { }

    public struct Bar<T> where T : struct
    {
        public Bar(int id) { value = id; thing = default(T); }

        public int value;
        public T thing;
    }
}

如果你运行程序,你会发现 en FooStruct 显然有 0 个字节的数据会消耗 1 个字节的内存。这对我来说是个问题的原因是我希望 Bar&lt;FooStruct&gt; 正好消耗 4 个字节(因为我要分配很多)。

为什么它会有这种行为,有没有办法解决这个问题(例如,有没有消耗 0 字节的特殊东西——我不是在寻找重新设计)?

【问题讨论】:

  • P.S.:[StructLayout(LayoutKind.Explicit, Size=0)] 给出相同的结果。
  • GC.GetTotalMemory 准确吗?如果是这样,我在内存分析器上浪费了钱。
  • 为什么不直接跳过 Bar 并让 FooStruct 包含公共 int 值?
  • 允许值有 0 个字节会创建一种吞噬黑洞的奇点。由于数组是一个明显的受害者,它的所有元素都将具有相同的地址。不要使用极端情况进行测试,而是针对您关心的结构进行测试。

标签: c# .net generics memory-management value-type


【解决方案1】:

总结:.NET 中的空结构占用 1 个字节。您可以将其视为packing,因为未命名字节只能通过不安全代码访问。

更多信息:如果您根据 .NET 报告的值执行所有指针运算,则一切都会一致。

以下示例说明了在堆栈上使用相邻的 0 字节结构,但这些观察显然也适用于 0 字节结构的数组。

struct z { };

unsafe static void foo()
{
    var z3 = default(z);
    bool _;
    long cb_pack, Δz, cb_raw;
    var z2 = default(z);    // (reversed since stack offsets are negative)
    var z1 = default(z);
    var z0 = default(z);

    // stack packing differs between x64 and x86
    cb_pack = (long)&z1 - (long)&z0; // --> 1 on x64, 4 on x86

    // pointer arithmetic should give packing in units of z-size
    Δz = &z1 - &z0; // --> 1 on x64, 4 on x86

    // if one asks for the value of such a 'z-size'...
    cb_raw = Marshal.SizeOf(typeof(z));     // --> 1

    // ...then the claim holds up:
    _ = cb_pack == Δz * cb_raw;     // --> true

    // so you cannot rely on special knowledge that cb_pack==0 or cb_raw==0
    _ = &z0 /* + 0 */ == &z1;   // --> false
    _ = &z0 /* + 0 + 0 */ == &z2;   // --> false

    // instead, the pointer arithmetic you meant was:
    _ = &z0 + cb_pack == &z1;   // --> true
    _ = &z0 + cb_pack + cb_pack == &z2; // --> true

    // array indexing also works using reported values
    _ = &(&z0)[Δz] == &z1;  // --> true

    // the default structure 'by-value' comparison asserts that
    // all z instances are (globally) equivalent...
    _ = EqualityComparer<z>.Default.Equals(z0, z1); // --> true

    // ...even when there are intervening non-z objects which
    // would prevent putative 'overlaying' of 0-sized structs:
    _ = EqualityComparer<z>.Default.Equals(z0, z3); // --> true

    // same result with boxing/unboxing
    _ = Object.Equals(z0, z3);  // -> true

    // this one is never true for boxed value types
    _ = Object.ReferenceEquals(z0, z0); // -> false
}

正如我在评论中提到的,@supercat 说得对,“从一开始就设计 .NET 以允许零长度结构可能不会有任何问题,但可能会有一些问题如果现在就开始这样做,那就会崩溃。”

编辑:如果您需要以编程方式区分 0 字节和 1 字节值类型,可以使用以下内容:

public static bool IsZeroSizeStruct(Type t)
{
    return t.IsValueType && !t.IsPrimitive && 
           t.GetFields((BindingFlags)0x34).All(fi => IsZeroSizeStruct(fi.FieldType));
}

请注意,这可以正确识别总大小为零的任意嵌套结构。

[StructLayout(LayoutKind.Sequential)]
struct z { };
[StructLayout(LayoutKind.Sequential)]
struct zz { public z _z, __z, ___z; };
[StructLayout(LayoutKind.Sequential)]
struct zzz { private zz _zz; };
[StructLayout(LayoutKind.Sequential)]
struct zzzi { public zzz _zzz; int _i; };

/// ...

c = Marshal.SizeOf(typeof(z));      // 1
c = Marshal.SizeOf(typeof(zz));     // 3
c = Marshal.SizeOf(typeof(zzz));    // 3
c = Marshal.SizeOf(typeof(zzzi));   // 8

_ = IsZeroSizeStruct(typeof(z));    // true
_ = IsZeroSizeStruct(typeof(zz));   // true 
_ = IsZeroSizeStruct(typeof(zzz));  // true
_ = IsZeroSizeStruct(typeof(zzzi)); // false

[edit: see comment] 这里奇怪的是,当嵌套 0 字节结构时,单字节最小值可以累积(即,对于 'zz' 和 'zzz' 为 3 个字节),然后突然全部只要包含一个“实质性”字段,这些谷壳就会消失。

【讨论】:

  • 你是什么意思,“箔条消失了”? zzzi 是 8 个字节,其中只有 4 个是“实质性字段”。填充并没有消失,它被填充了 more 以正确对齐int
  • @BenVoigt 谢谢,你是对的。我想我可能误以为int _i 在这里是long,因此占据了sizeof zzzi 报告的全部8 个字节。那会很神秘。我已经更新了我的帖子。
【解决方案2】:

这与 C(或 C++)中不允许使用零大小对象的原因相同:就元素数量而言的指针算术。

C# 支持不安全块中的指针减法,定义如下:

给定两个表达式PQ,指针类型为T*,表达式P – Q 计算PQ 给出的地址之间的差,然后将该差除以@ 987654327@.

由于不可能除以零,这意味着sizeof(T) &gt; 0 对所有T

【讨论】:

    【解决方案3】:

    这是你要找的吗?

    Null / Empty value for a struct in .Net 1.x

    此解决方案提到没有任何开销,我相信这就是您正在寻找的。​​p>

    此外,Stroustrup 还谈到了为什么 C++ 中的结构不是空的,现在语言不同了,但原理是一样的:http://www.stroustrup.com/bs_faq2.html#sizeof-empty

    【讨论】:

    • 在 C++ 中,假定每个对象都有一个由其地址封装的不同标识。因此,每个对象都必须使分配给它的地址对任何其他对象都不可用。最简单的方法是让每个对象至少占用一个可寻址单元。我认为在 .NET 中不需要这样的事情,因为我不知道有任何比较 byref 是否相等的范例。
    • @supercat 我也想过这个问题......但也许它确实有道理——毕竟,如果大小真的为 0,你应该如何在不安全的代码中编写迭代器等。
    • @kirk 抱歉,在这种情况下,您的解决方案完全是错误的。乔恩是对的,你应该再次阅读他的评论和他的问题。
    • @StefandeBruijn:我不知道有任何论据会迫使我在 .net 的设计中强加 struct 的最小大小。由于没有一种“仅限原始”的泛型类型参数,我看不出代码如何在不知道底层类型的情况下尝试循环遍历固定数组。此外,即使有人正在编写代码以使用 byrefs 对固定数组中的连续元素重复调用方法,.net 中的每个数组都有一个基于项目的长度,该长度不依赖于其物理分配。 for(i=0, ptr=array_data; i&lt;array_length; ptr+=array_item_size, i++) do_something(ref *ptr);
    • @StefandeBruijn: ...即使array_item_size 为零,也应该可以正常工作。 ptr 值不会去任何地方,但那又怎样?也许有人会说,比较某种类型的指针的能力意味着该类型必须具有非零大小,但我不确定何时真正需要比较未知或泛型类型的指针。
    猜你喜欢
    • 1970-01-01
    • 2012-01-11
    • 1970-01-01
    • 2019-09-30
    • 2018-10-10
    • 1970-01-01
    • 2011-05-10
    • 1970-01-01
    相关资源
    最近更新 更多