2013-05-17 12 views
11

Ho sempre capito che le strutture (tipi di valori) contengono esattamente il numero di byte definito nei campi della struttura ... tuttavia, ho fatto alcuni test e sembra esserci un'eccezione per le struct vuoti:Perché una struttura vuota in C# consuma memoria

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; 
    } 
} 

Se si esegue il programma, troverete che en FooStruct che ovviamente ha 0 byte di dati consumerà 1 byte di memoria. Il motivo per cui questo è un problema per me è che voglio che lo Bar<FooStruct> consumi esattamente 4 byte (perché ho intenzione di allocarlo molto).

Perché ha questo comportamento ed esiste un modo per risolvere questo problema (ad esempio c'è una cosa speciale che consuma 0 byte-- Non sto cercando una riprogettazione)?

+0

P.S .: [StructLayout (LayoutKind.Explicit, Size = 0)] fornisce lo stesso risultato. – atlaste

+1

GC.GetTotalMemory è preciso? Se è così ho sprecato soldi su un profiler di memoria. – Paparazzi

+0

Perché non saltare semplicemente Bar e avere FooStruct include il valore int pubblico? – Paparazzi

risposta

8

Riassunto: Uno struct vuoto nel .NET consuma 1 byte. Si può pensare a questo come packing, poiché il byte senza nome è accessibile solo tramite codice non sicuro.

: se si esegue l'aritmetica dei puntatori in base ai valori riportati da .NET, le cose funzionano in modo coerente.

Il seguente esempio illustra l'utilizzo di strutture adiacenti a 0 byte nello stack, ma queste osservazioni si applicano ovviamente anche agli array di strutture a 0 byte.

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 
} 

Come ho già detto in un commento, @supercat azzeccato quando ha osservato, "Probabilmente non sarebbe stato alcun problema con la progettazione di .NET per consentire alle strutture di lunghezza zero fin dall'inizio, ma ci potrebbero essere alcune cose che si spezzerebbero se iniziasse a farlo ora ".

EDIT: Se è necessario distinguere tra la programmazione 0 byte tipi di valore vs 1 byte, è possibile utilizzare il seguente:

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

notare che questa struct identifica correttamente arbitrariamente annidate in cui il totale la dimensione sarebbe zero.

[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: vedi commento] Cosa c'è di strano è che, quando la nidificazione struct 0 byte, il minimo a byte singolo può accumulare (cioè in 3 byte per 'zz' e 'zzz'), ma poi improvvisamente tutto questo scompare non appena viene incluso un singolo campo "sostanziale".

+0

Cosa intendi con "chaff scompare"? 'zzzi' è 8 byte, solo 4 dei quali è il" campo sostanziale ". Il padding non è scomparso, è stato riempito * di più * per allineare correttamente il 'int'. –

+0

@BenVoigt Grazie, sei corretto. Penso che potrei aver erroneamente pensato che 'int _i' fosse un' long' qui, quindi occupando tutti gli 8 byte riportati da 'sizeof zzzi'. Sarebbe stato misterioso. Ho aggiornato il mio post. –

0

È questo quello che stai cercando?

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

Questa soluzione non menziona avere alcun overhead, che credo sia quello che stai cercando.

Inoltre, Stroustrup parla perché le strutture non sono vuoti in C++, ora la lingua è diversa, ma il principio è lo stesso: http://www.stroustrup.com/bs_faq2.html#sizeof-empty

+1

In C++, si presume che ogni oggetto abbia un'identità distinta incapsulata dal suo indirizzo. Pertanto, ogni oggetto deve rendere non disponibile l'indirizzo che gli è assegnato a qualsiasi altro oggetto. Il modo più semplice per farlo è fare in modo che ogni oggetto occupi almeno una unità indirizzabile. Non credo che una cosa del genere dovrebbe essere necessaria in .NET, dal momento che non sono a conoscenza di alcun paradigma di confronto di byrif per l'uguaglianza. – supercat

+0

@supercat Ci ho pensato anche io ... ma forse ha senso - dopotutto, come dovresti scrivere iteratori e così via in codice non sicuro se la dimensione è davvero 0. – atlaste

+0

@kirk scusa, sei la soluzione è semplicemente sbagliata in questo scenario. Jon ha ragione, dovresti leggere di nuovo il suo commento e la sua domanda. – atlaste

8

È la stessa ragione per cui gli oggetti a dimensione zero non sono consentiti in C (o C++): aritmetica del puntatore in termini di numero di elementi.

C# supporta puntatore sottrazione in blocchi non sicuri, così definito:

Date due espressioni, P e Q, di un tipo puntatore T*, l'espressione P – Q calcola la differenza tra gli indirizzi dati dal P e Q e quindi divide tale differenza per sizeof(T).

Poiché la divisione per zero non è possibile, ciò implica che sizeof(T) > 0 per tutti T.

Problemi correlati