Почему пустая структура в С# потребляет память

Я всегда понимал, что structs (типы значений) содержат точно количество байтов, как определено в полях структуры... однако я сделал несколько тестов и, похоже, существует исключение для пустых структур:

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<FooStruct> потреблял ровно 4 байта (потому что я собираюсь выделить его много).

Почему у этого есть это поведение, и есть ли способ исправить это (например, есть ли что-то особенное, которое потребляет 0 байт - я не ищу редизайн)?

Ответ 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, чтобы позволить структурам с нулевой длиной с самого начала, но могут быть некоторые вещи это сломалось бы, если бы оно начинало делать это сейчас".

EDIT: Если вам необходимо программно различать 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-байтовых структурах однобайтовый минимум может накапливаться (т.е. в 3 байта для "zz" и "zzz" ), но затем внезапно все эта мякина исчезает, как только включается одно "существенное" поле.

Ответ 2

По той же причине объекты с нулевым размером не допускаются в C (или С++): арифметика указателя по количеству элементов.

С# поддерживает вычитание указателя в небезопасных блоках, определяемое таким образом:

Для двух выражений P и Q типа указателя T* выражение P – Q вычисляет разницу между адресами, заданными P и Q, а затем делит эту разность на sizeof(T).

Так как деление на ноль невозможно, это означает, что sizeof(T) > 0 для всех T.

Ответ 3

Это то, что вы ищете?

Значение Null/Empty для структуры в .Net 1.x

В этом решении упоминается отсутствие каких-либо накладных расходов, которые, я считаю, являются тем, что вы ищете.

Кроме того, Stroustrup говорит о том, почему структуры не пусты в С++, теперь язык отличается, но принцип тот же: http://www.stroustrup.com/bs_faq2.html#sizeof-empty