Trova la dimensione dell'istanza dell'oggetto in byte in c #


114

Per qualsiasi istanza arbitraria (raccolte di diversi oggetti, composizioni, singoli oggetti, ecc.)

Come posso determinare la sua dimensione in byte?

(Al momento ho una raccolta di vari oggetti e sto cercando di determinarne la dimensione aggregata)

EDIT: qualcuno ha scritto un metodo di estensione per Object che potrebbe farlo? Sarebbe abbastanza carino imo.



Risposte:


60

Prima di tutto, un avvertimento: ciò che segue è strettamente nel regno degli hack brutti e non documentati. Non fare affidamento su questo funzionamento: anche se funziona per te ora, potrebbe smettere di funzionare domani, con qualsiasi aggiornamento .NET minore o maggiore.

È possibile utilizzare le informazioni contenute in questo articolo sugli elementi interni di CLR MSDN Magazine Numero 2005, maggio - Esaminare gli elementi interni di .NET Framework per vedere come CLR crea oggetti runtime - l'ultima volta che ho controllato, era ancora applicabile. Ecco come si fa (recupera il campo interno "Basic Instance Size" tramite TypeHandleil tipo).

object obj = new List<int>(); // whatever you want to get the size of
RuntimeTypeHandle th = obj.GetType().TypeHandle;
int size = *(*(int**)&th + 1);
Console.WriteLine(size);

Funziona su 3.5 SP1 a 32 bit. Non sono sicuro che le dimensioni dei campi siano le stesse su 64 bit: potresti dover regolare i tipi e / o gli offset se non lo sono.

Questo funzionerà per tutti i tipi "normali", per i quali tutte le istanze hanno gli stessi tipi ben definiti. Quelli per cui questo non è vero sono sicuramente array e stringhe, e credo anche StringBuilder. Per loro dovrai aggiungere la dimensione di tutti gli elementi contenuti alla dimensione dell'istanza di base.


No. Non esiste un modo "appropriato" per farlo, perché non è qualcosa di cui dovrebbe preoccuparsi in primo luogo un'applicazione .NET ben comportata. Quanto sopra si integra direttamente con le strutture dati interne di una particolare implementazione di CLR (che può facilmente cambiare nella prossima versione di .NET, per esempio).
Pavel Minaev

3
dovrebbe funzionare in C # o solo in c ++ gestito? non è felice in C # finora che l'ho provato:Cannot take the address of, get the size of, or declare a pointer to a managed type ('System.RuntimeTypeHandle')
Maslow

17
La versione .NET 4 di questo non ha nemmeno bisogno di codice non sicuro: Marshal.ReadInt32(type.TypeHandle.Value, 4)funziona per x86 e x64. Ho testato solo tipi di struttura e classe. Tieni presente che questo restituisce la dimensione in scatola per i tipi di valore. @Pavel Forse potresti aggiornare la tua risposta.
jnm2

2
@ sab669 beh, sostituisci typecon obj.GetType()nel suo esempio. Non importa quale framework stai utilizzando, solo quale CLR (v2 o v4 o CoreCLR). Non l'ho provato su CoreCLR.
jnm2

2
@SamGoldberg Calcolarlo manualmente richiede molto lavoro con un milione di casi limite. Sizeof ti dice la dimensione statica di un oggetto, non il consumo di memoria di un grafico di runtime di oggetti. La memoria di VS2017 e il profilo della CPU sono molto buoni, così come lo sono ReSharper e altri strumenti, ed è quello che userei per misurare.
jnm2

20

Potresti essere in grado di approssimare la dimensione fingendo di serializzarla con un serializzatore binario (ma instradando l'output all'oblio) se stai lavorando con oggetti serializzabili.

class Program
{
    static void Main(string[] args)
    {
        A parent;
        parent = new A(1, "Mike");
        parent.AddChild("Greg");
        parent.AddChild("Peter");
        parent.AddChild("Bobby");

        System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf =
           new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
        SerializationSizer ss = new SerializationSizer();
        bf.Serialize(ss, parent);
        Console.WriteLine("Size of serialized object is {0}", ss.Length);
    }
}

[Serializable()]
class A
{
    int id;
    string name;
    List<B> children;
    public A(int id, string name)
    {
        this.id = id;
        this.name = name;
        children = new List<B>();
    }

    public B AddChild(string name)
    {
        B newItem = new B(this, name);
        children.Add(newItem);
        return newItem;
    }
}

[Serializable()]
class B
{
    A parent;
    string name;
    public B(A parent, string name)
    {
        this.parent = parent;
        this.name = name;
    }
}

class SerializationSizer : System.IO.Stream
{
    private int totalSize;
    public override void Write(byte[] buffer, int offset, int count)
    {
        this.totalSize += count;
    }

    public override bool CanRead
    {
        get { return false; }
    }

    public override bool CanSeek
    {
        get { return false; }
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override void Flush()
    {
        // Nothing to do
    }

    public override long Length
    {
        get { return totalSize; }
    }

    public override long Position
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        throw new NotImplementedException();
    }

    public override long Seek(long offset, System.IO.SeekOrigin origin)
    {
        throw new NotImplementedException();
    }

    public override void SetLength(long value)
    {
        throw new NotImplementedException();
    }
}

6
Naturalmente, questo può farti ottenere una dimensione minima, ma non ti dice nulla sulla dimensione della memoria.
John Saunders,

Lol, la prossima lampadina che avevo prima di tornare per controllare le risposte stava usando il serializzatore binario. John, come potrebbe questo non darti la dimensione effettiva della memoria?
Janie

2
Ti darebbe la dimensione serializzata, che sarà la dimensione che il serializzatore voleva, per scopi di "serializzatore". Probabilmente sono diversi dagli scopi del "sit-in-memory". Forse il serializzatore memorizza numeri interi più piccoli in tre byte, ad esempio.
John Saunders

4
Come ho detto, è solo un'approssimazione. Non è perfetto, ma non sono d'accordo sul fatto che non ti dica "nulla" sulla dimensione della memoria. Direi che si dato qualche idea - serializzazioni più grandi sarebbe generalmente correlata con dimensioni più grandi in memoria. C'è qualche relazione.
BlueMonkMN

Sono d'accordo: è utile ottenere una stima approssimativa delle dimensioni di un oggetto grafico .NET.
Craig Shearer

8

Per i tipi non gestiti, ovvero i tipi di valore, le strutture:

        Marshal.SizeOf(object);

Per gli oggetti gestiti, più mi avvicino è un'approssimazione.

        long start_mem = GC.GetTotalMemory(true);

        aclass[] array = new aclass[1000000];
        for (int n = 0; n < 1000000; n++)
            array[n] = new aclass();

        double used_mem_median = (GC.GetTotalMemory(false) - start_mem)/1000000D;

Non utilizzare la serializzazione Un formattatore binario aggiunge intestazioni, quindi puoi cambiare la tua classe e caricare un vecchio file serializzato nella classe modificata.

Inoltre non ti dirà la dimensione reale in memoria né terrà conto dell'allineamento della memoria.

[Modifica] Usando BiteConverter.GetBytes (prop-value) in modo ricorsivo su ogni proprietà della tua classe otterrai il contenuto in byte, che non conta il peso della classe o dei riferimenti ma è molto più vicino alla realtà. Consiglierei di utilizzare un array di byte per i dati e una classe proxy non gestita per accedere ai valori utilizzando il casting del puntatore se le dimensioni contano, nota che sarebbe una memoria non allineata, quindi sui vecchi computer sarà lento ma saranno enormi set di dati sulla RAM MODERNA notevolmente più veloce, poiché ridurre al minimo le dimensioni per leggere dalla RAM avrà un impatto maggiore rispetto a quello non allineato.


5

Questo non si applica all'attuale implementazione .NET, ma una cosa da tenere a mente con i runtime gestiti / raccolti dai rifiuti è che la dimensione allocata di un oggetto può cambiare per tutta la durata del programma. Ad esempio, alcuni raccoglitori di rifiuti generazionali (come il raccoglitore ibrido di conteggio di riferimento generazionale / ulteriore ) hanno bisogno di memorizzare determinate informazioni solo dopo che un oggetto è stato spostato dal nido allo spazio maturo.

Ciò rende impossibile creare un'API generica affidabile per esporre la dimensione dell'oggetto.


Interessante. Quindi cosa fanno le persone per determinare dinamicamente le dimensioni dei loro oggetti / raccolte di oggetti?
Janie

2
Dipende da cosa ne hanno bisogno. Se per P / Invoke (interoperabilità del codice nativo), usano Marshal.SizeOf (typeof (T)). Se per la profilazione della memoria, utilizzano un profiler separato che collabora con l'ambiente di esecuzione per fornire le informazioni. Se sei interessato all'allineamento degli elementi in un array, puoi utilizzare il codice operativo SizeOf IL in un DynamicMethod (non credo che ci sia un modo più semplice nel framework .NET per questo).
Sam Harwell

5

soluzione sicura con alcune ottimizzazioni CyberSaving / MemoryUsage code . qualche caso:

/* test nullable type */      
TestSize<int?>.SizeOf(null) //-> 4 B

/* test StringBuilder */    
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) sb.Append("わたしわたしわたしわ");
TestSize<StringBuilder>.SizeOf(sb ) //-> 3132 B

/* test Simple array */    
TestSize<int[]>.SizeOf(new int[100]); //-> 400 B

/* test Empty List<int>*/    
var list = new List<int>();  
TestSize<List<int>>.SizeOf(list); //-> 205 B

/* test List<int> with 100 items*/
for (int i = 0; i < 100; i++) list.Add(i);
TestSize<List<int>>.SizeOf(list); //-> 717 B

Funziona anche con le classi:

class twostring
{
    public string a { get; set; }
    public string b { get; set; }
}
TestSize<twostring>.SizeOf(new twostring() { a="0123456789", b="0123456789" } //-> 28 B

Questo è l'approccio anche io. È possibile aggiungere una serie di oggetti incontrati in precedenza in un grafico per evitare a) ricorsione infinita eb) evitare di aggiungere due volte la stessa memoria.
mafu

4

Questo è impossibile da fare in fase di esecuzione.

Tuttavia, esistono vari profili di memoria che visualizzano le dimensioni degli oggetti.

EDIT : Potresti scrivere un secondo programma che profili il primo utilizzando l' API di profilazione CLR e comunichi con esso tramite il telecomando o qualcosa del genere.


17
Se è impossibile farlo in fase di esecuzione, come forniscono le informazioni i profiler della memoria?
Janie

2
Utilizzando l'API di profilazione. Tuttavia, un programma non può profilarsi da solo
SLaks

Interessante. E se volessi che il codice si occupi dei casi in cui gli oggetti consumano troppa memoria?
Janie

4
Allora avresti a che fare con software autocosciente e sarei molto spaventato. :-) Seriamente, "responsabile unico": lascia che il programma sia il programma, che qualche altra parte di codice controlli gli oggetti che occupano troppa memoria.
John Saunders

2
@Janie: faresti anche delle supposizioni sul significato delle dimensioni e su come si relazionano alla performance. Penso che vorresti essere un vero esperto di prestazioni CLR di basso livello (il tipo che conosce già l'API di profilazione) prima di farlo. Altrimenti, potresti applicare le tue precedenti esperienze a una situazione in cui non si applicano.
John Saunders,


2

AFAIK, non è possibile, senza effettivamente contare la dimensione di ciascun membro in byte. Ma ancora una volta, la dimensione di un membro (come gli elementi all'interno di una raccolta) conta per la dimensione dell'oggetto o un puntatore a quel membro conta per la dimensione dell'oggetto? Dipende da come lo definisci.

Mi sono imbattuto in questa situazione prima in cui volevo limitare gli oggetti nella mia cache in base alla memoria che consumavano.

Bene, se c'è qualche trucco per farlo, sarei felice di saperlo!


2

Per i tipi di valore, puoi usare Marshal.SizeOf. Naturalmente, restituisce il numero di byte necessari per effettuare il marshalling della struttura nella memoria non gestita, che non è necessariamente ciò che utilizza il CLR.


SizeOf (Object) potrebbe non essere disponibile nelle versioni future. Utilizzare invece SizeOf <T> (). Per maggiori informazioni, visita go.microsoft.com/fwlink/?LinkID=296514
Vinigas

1

È possibile utilizzare la reflection per raccogliere tutte le informazioni sui membri o sulle proprietà pubbliche (dato il tipo di oggetto). Tuttavia, non è possibile determinare la dimensione senza passare attraverso ogni singolo dato sull'oggetto.


1

Per chi cerca una soluzione che non richieda [Serializable]lezioni e dove il risultato sia un'approssimazione invece che una scienza esatta. Il metodo migliore che ho trovato è la serializzazione json in un flusso di memoria utilizzando la codifica UTF32.

private static long? GetSizeOfObjectInBytes(object item)
{
    if (item == null) return 0;
    try
    {
        // hackish solution to get an approximation of the size
        var jsonSerializerSettings = new JsonSerializerSettings
        {
            DateFormatHandling = DateFormatHandling.IsoDateFormat,
            DateTimeZoneHandling = DateTimeZoneHandling.Utc,
            MaxDepth = 10,
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
        };
        var formatter = new JsonMediaTypeFormatter { SerializerSettings = jsonSerializerSettings };
        using (var stream = new MemoryStream()) { 
            formatter.WriteToStream(item.GetType(), item, stream, Encoding.UTF32);
            return stream.Length / 4; // 32 bits per character = 4 bytes per character
        }
    }
    catch (Exception)
    {
        return null;
    }
}

No, questo non ti darà la dimensione esatta che verrebbe utilizzata in memoria. Come accennato in precedenza, ciò non è possibile. Ma ti darà una stima approssimativa.

Nota che anche questo è piuttosto lento.


1

Da Pavel e jnm2:

private int DumpApproximateObjectSize(object toWeight)
{
   return Marshal.ReadInt32(toWeight.GetType().TypeHandle.Value, 4);
}

In una nota a margine, fai attenzione perché funziona solo con oggetti di memoria contigui


1

Ho creato test di benchmark per diverse raccolte in .NET: https://github.com/scholtz/TestDotNetCollectionsMemoryAllocation

I risultati sono i seguenti per .NET Core 2.2 con 1.000.000 di oggetti con 3 proprietà allocate:

Testing with string: 1234567
Hashtable<TestObject>:                                     184 672 704 B
Hashtable<TestObjectRef>:                                  136 668 560 B
Dictionary<int, TestObject>:                               171 448 160 B
Dictionary<int, TestObjectRef>:                            123 445 472 B
ConcurrentDictionary<int, TestObject>:                     200 020 440 B
ConcurrentDictionary<int, TestObjectRef>:                  152 026 208 B
HashSet<TestObject>:                                       149 893 216 B
HashSet<TestObjectRef>:                                    101 894 384 B
ConcurrentBag<TestObject>:                                 112 783 256 B
ConcurrentBag<TestObjectRef>:                               64 777 632 B
Queue<TestObject>:                                         112 777 736 B
Queue<TestObjectRef>:                                       64 780 680 B
ConcurrentQueue<TestObject>:                               112 784 136 B
ConcurrentQueue<TestObjectRef>:                             64 783 536 B
ConcurrentStack<TestObject>:                               128 005 072 B
ConcurrentStack<TestObjectRef>:                             80 004 632 B

Per test di memoria ho trovato il migliore da utilizzare

GC.GetAllocatedBytesForCurrentThread()

1

Per gli array di strutture / valori, ho risultati diversi con:

first = Marshal.UnsafeAddrOfPinnedArrayElement(array, 0).ToInt64();
second = Marshal.UnsafeAddrOfPinnedArrayElement(array, 1).ToInt64();
arrayElementSize = second - first;

(esempio semplificato)

Qualunque sia l'approccio, devi davvero capire come funziona .Net per interpretare correttamente i risultati. Ad esempio, la dimensione dell'elemento restituito è la dimensione dell'elemento "allineato", con un certo riempimento. L'overhead e quindi la dimensione è diverso a seconda dell'utilizzo di un tipo: "boxed" sull'heap GC, sullo stack, come un campo, come un elemento dell'array.

(Volevo sapere quale sarebbe l'impatto sulla memoria dell'uso di strutture vuote "fittizie" (senza alcun campo) per imitare argomenti "opzionali" dei generici; facendo test con layout diversi che coinvolgono strutture vuote, posso vedere che una struttura vuota usa ( almeno) 1 byte per elemento; Ricordo vagamente che è perché .Net necessita di un indirizzo diverso per ogni campo, cosa che non funzionerebbe se un campo fosse veramente vuoto / di dimensione 0).


0

Il modo più semplice è: int size = *((int*)type.TypeHandle.Value + 1)

So che questo è un dettaglio di implementazione, ma GC fa affidamento su di esso e deve essere il più vicino all'inizio della tabella dei metodi per l'efficienza, oltre a prendere in considerazione quanto sia complesso il codice GC nessuno oserà cambiarlo in futuro. Infatti funziona per tutte le versioni minori / maggiori di .net framework + .net core. (Attualmente non è possibile testare per 1.0)
Se si desidera un modo più affidabile, emettere una struttura in un assembly dinamico [StructLayout(LayoutKind.Auto)]con esattamente gli stessi campi nello stesso ordine, prendere la sua dimensione con l' istruzione sizeof IL. Potresti voler emettere un metodo statico all'interno di struct che restituisca semplicemente questo valore. Quindi aggiungi 2 * IntPtr.Size per l'intestazione dell'oggetto. Questo dovrebbe darti un valore esatto.
Ma se la tua classe deriva da un'altra classe, devi trovare ogni dimensione della classe base separatamente e aggiungerle di nuovo + 2 * Inptr.Size per l'intestazione. Puoi farlo ottenendo campi con BindingFlags.DeclaredOnlyflag.
Array e stringhe aggiungono semplicemente quella dimensione alla sua lunghezza * dimensione dell'elemento. Per dimensioni cumulative di oggetti aggreagate è necessario implementare una soluzione più sofisticata che coinvolga la visita di ogni campo e l'ispezione del suo contenuto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.