Rechercher la taille de l'instance d'objet en octets en c #


114

Pour toute instance arbitraire (collections d'objets différents, compositions, objets uniques, etc.)

Comment puis-je déterminer sa taille en octets?

(J'ai actuellement une collection d'objets divers et j'essaye de déterminer la taille agrégée de celui-ci)

EDIT: Quelqu'un a-t-il écrit une méthode d'extension pour Object qui pourrait le faire? Ce serait plutôt chouette imo.



Réponses:


60

Tout d'abord, un avertissement: ce qui suit est strictement du domaine des hacks laids et non documentés. Ne comptez pas sur ce fonctionnement - même si cela fonctionne pour vous maintenant, il peut cesser de fonctionner demain, avec une mise à jour mineure ou majeure de .NET.

Vous pouvez utiliser les informations contenues dans cet article sur les composants internes de CLR MSDN Magazine Numéro 2005 mai - Explorer les composants internes de .NET Framework pour voir comment le CLR crée des objets d'exécution - la dernière fois que j'ai vérifié, il était toujours applicable. Voici comment procéder (il récupère le champ interne "Basic Instance Size" via TypeHandledu type).

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

Cela fonctionne sur 3.5 SP1 32 bits. Je ne sais pas si les tailles de champ sont les mêmes sur 64 bits - vous devrez peut-être ajuster les types et / ou les décalages s'ils ne le sont pas.

Cela fonctionnera pour tous les types "normaux", pour lesquels toutes les instances ont les mêmes types bien définis. Ceux pour lesquels ce n'est pas vrai sont des tableaux et des chaînes, et je crois aussi StringBuilder. Pour eux, vous devrez ajouter la taille de tous les éléments contenus à leur taille d'instance de base.


Non. Il n'y a pas de manière «appropriée» de faire cela, car ce n'est pas quelque chose dont une application .NET bien comportée devrait se préoccuper en premier lieu. Ce qui précède mucks directement avec les structures de données internes d'une implémentation particulière de CLR (qui peut facilement changer dans la prochaine version de .NET, par exemple).
Pavel Minaev

3
est-ce censé fonctionner en C # ou uniquement en C ++ géré? ce n'est pas content en C # pour l'instant que je l'ai essayé:Cannot take the address of, get the size of, or declare a pointer to a managed type ('System.RuntimeTypeHandle')
Maslow

17
La version .NET 4 de cela n'a même pas besoin de code non sécurisé: Marshal.ReadInt32(type.TypeHandle.Value, 4)fonctionne pour x86 et x64. Je n'ai testé que les types struct et class. Gardez à l'esprit que cela renvoie la taille encadrée pour les types valeur. @Pavel Vous pourriez peut-être mettre à jour votre réponse.
jnm2

2
@ sab669 bien, remplacez typepar obj.GetType()dans son exemple. Peu importe le framework que vous utilisez, seulement le CLR (v2 ou v4 ou CoreCLR). Je n'ai pas essayé cela sur CoreCLR.
jnm2

2
@SamGoldberg Le calcul manuel est beaucoup de travail avec un million de cas extrêmes. Sizeof vous indique la taille statique d'un objet, et non la consommation de mémoire d'un graphe d'objets à l'exécution. Le profilage de la mémoire et du processeur de VS2017 est très bon, tout comme ReSharper et d'autres outils, et c'est ce que j'utiliserais pour mesurer.
jnm2

21

Vous pourrez peut-être approximer la taille en prétendant la sérialiser avec un sérialiseur binaire (mais en acheminant la sortie vers l'oubli) si vous travaillez avec des objets sérialisables.

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
Bien sûr, cela peut vous donner une taille minimale, mais ne vous dit rien sur la taille en mémoire.
John Saunders le

Lol, la prochaine ampoule que j'avais avant de revenir vérifier les réponses utilisait le sérialiseur binaire. John, comment cela ne vous donnerait-il pas la taille réelle en mémoire?
Janie

2
Cela vous donnerait la taille sérialisée, qui sera la taille voulue par le sérialiseur, à des fins de "sérialiseur". Celles-ci sont probablement différentes des objectifs «sit-in-memory». Peut-être que le sérialiseur stocke des entiers plus petits sur trois octets, par exemple.
John Saunders le

4
Comme je l'ai dit, ce n'est qu'une approximation. Ce n'est pas parfait, mais je ne suis pas d'accord pour dire que cela ne vous dit «rien» sur la taille de la mémoire. Je dirais que cela vous a donné une idée - des sérialisations plus importantes seraient généralement corrélées avec des tailles en mémoire plus importantes. Il y a une certaine relation.
BlueMonkMN

Je suis d'accord - il est utile d'obtenir une estimation approximative de la taille d'un graphique d'objets .NET.
Craig Shearer

8

Pour les types non managés aka types valeur, structs:

        Marshal.SizeOf(object);

Pour les objets gérés, plus je m'approche est une approximation.

        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;

N'utilisez pas de sérialisation.Un formateur binaire ajoute des en-têtes, vous pouvez donc modifier votre classe et charger un ancien fichier sérialisé dans la classe modifiée.

De plus, il ne vous indiquera pas la taille réelle de la mémoire ni ne prendra en compte l'alignement de la mémoire.

[Edit] En utilisant BiteConverter.GetBytes (prop-value) de manière récursive sur chaque propriété de votre classe, vous obtiendriez le contenu en octets, cela ne compte pas le poids de la classe ou des références mais est beaucoup plus proche de la réalité. Je recommanderais d'utiliser un tableau d'octets pour les données et une classe proxy non gérée pour accéder aux valeurs à l'aide de la conversion de pointeur si la taille compte, notez que ce serait une mémoire non alignée, donc sur les vieux ordinateurs, ce sera lent, mais d'énormes ensembles de données sur la RAM MODERNE vont être considérablement plus rapide, car minimiser la taille de lecture à partir de la RAM aura un impact plus important que non aligné.


5

Cela ne s'applique pas à l'implémentation .NET actuelle, mais une chose à garder à l'esprit avec les environnements d'exécution récupérés / gérés est que la taille allouée d'un objet peut changer tout au long de la durée de vie du programme. Par exemple, certains garbage collector générationnel (tels que le collecteur hybride de comptage de référence générationnel / ultérieur ) n'ont besoin de stocker certaines informations qu'après le déplacement d'un objet de la pépinière vers l'espace mature.

Cela rend impossible la création d'une API générique fiable pour exposer la taille de l'objet.


Intéressant. Alors, que font les gens pour déterminer dynamiquement la taille de leurs objets / collections d'objets?
Janie le

2
Cela dépend de ce pour quoi ils en ont besoin. Si pour P / Invoke (interopérabilité de code natif), ils utilisent Marshal.SizeOf (typeof (T)). Si pour le profilage de la mémoire, ils utilisent un profileur distinct qui coopère avec l'environnement d'exécution pour fournir les informations. Si vous êtes intéressé par l'alignement des éléments dans un tableau, vous pouvez utiliser l'opcode SizeOf IL dans un DynamicMethod (je ne pense pas qu'il existe un moyen plus simple dans le framework .NET pour cela).
Sam Harwell

5

solution sûre avec quelques optimisations CyberSaving / MemoryUsage code . un cas:

/* 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

Cela fonctionne également avec les classes:

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

C'est aussi l'approche que j'adopterais. Vous pouvez ajouter un ensemble d'objets précédemment rencontrés dans un graphe pour éviter a) une récursion infinie et b) éviter d'ajouter deux fois la même mémoire.
mafu

4

Ceci est impossible à faire lors de l'exécution.

Il existe cependant différents profileurs de mémoire qui affichent la taille de l'objet.

EDIT : Vous pouvez écrire un deuxième programme qui profile le premier à l'aide de l' API de profilage CLR et communique avec lui via la communication à distance ou autre.


17
S'il est impossible de le faire au moment de l'exécution, comment les profileurs de mémoire fournissent-ils les informations?
Janie le

2
En utilisant l'API de profilage. Cependant, un programme ne peut pas se profiler
SLaks

Intéressant. Et si je voulais que le code traite les cas où les objets consommaient trop de mémoire?
Janie le

4
Vous auriez alors affaire à un logiciel auto-conscient, et j'aurais très peur. :-) Sérieusement, "responsabilité principale unique" - laissez le programme être le programme, laissez un autre morceau de code surveiller les objets qui prennent trop de mémoire.
John Saunders le

2
@Janie: vous feriez également des hypothèses sur l'importance de la taille et son lien avec la performance. Je pense que vous voudriez être un véritable expert en performances CLR de bas niveau (le genre qui connaît déjà l'API de profilage) avant de le faire. Sinon, vous pourriez appliquer vos expériences antérieures à une situation dans laquelle elles ne s'appliquent pas.
John Saunders le


2

AFAIK, vous ne pouvez pas, sans compter réellement la taille de chaque membre en octets. Mais encore une fois, la taille d'un membre (comme les éléments à l'intérieur d'une collection) compte-t-elle dans la taille de l'objet, ou un pointeur vers ce membre compte-t-il dans la taille de l'objet? Cela dépend de la façon dont vous le définissez.

J'ai déjà rencontré cette situation où je voulais limiter les objets dans mon cache en fonction de la mémoire qu'ils consommaient.

Eh bien, s'il y a une astuce pour faire ça, je serais ravi de le savoir!


2

Pour les types valeur, vous pouvez utiliser Marshal.SizeOf. Bien sûr, il renvoie le nombre d'octets requis pour rassembler la structure en mémoire non managée, ce qui n'est pas nécessairement ce que le CLR utilise.


SizeOf (Object) peut ne pas être disponible dans les versions futures. À la place, utilisez SizeOf <T> (). Pour plus d'informations, rendez-vous sur go.microsoft.com/fwlink/?LinkID=296514
Vinigas

1

Vous pouvez utiliser la réflexion pour rassembler toutes les informations publiques sur les membres ou les propriétés (en fonction du type de l'objet). Cependant, il n'y a aucun moyen de déterminer la taille sans parcourir chaque élément de données individuel sur l'objet.


1

Pour tous ceux qui recherchent une solution qui ne nécessite pas de [Serializable]cours et où le résultat est une approximation au lieu de la science exacte. La meilleure méthode que j'ai pu trouver est la sérialisation json dans un flux de mémoire en utilisant le codage 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;
    }
}

Non, cela ne vous donnera pas la taille exacte qui serait utilisée en mémoire. Comme mentionné précédemment, ce n'est pas possible. Mais cela vous donnera une estimation approximative.

Notez que c'est également assez lent.


1

Depuis Pavel et jnm2:

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

Sur une note latérale, soyez prudent car il ne fonctionne qu'avec des objets de mémoire contigus


1

J'ai créé un test de référence pour différentes collections dans .NET: https://github.com/scholtz/TestDotNetCollectionsMemoryAllocation

Les résultats sont les suivants pour .NET Core 2.2 avec 1000000 d'objets avec 3 propriétés allouées:

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

Pour le test de mémoire, j'ai trouvé le meilleur à utiliser

GC.GetAllocatedBytesForCurrentThread()

1

Pour les tableaux de structures / valeurs, j'ai des résultats différents avec:

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

(exemple simplifié à l'extrême)

Quelle que soit l'approche, vous devez vraiment comprendre comment .Net fonctionne pour interpréter correctement les résultats. Par exemple, la taille de l'élément retourné est la taille de l'élément "aligné", avec un peu de remplissage. La surcharge et donc la taille sont différentes selon l'utilisation d'un type: "boxed" sur le tas GC, sur la pile, comme un champ, comme un élément de tableau.

(Je voulais savoir quel serait l'impact sur la mémoire de l'utilisation de structures vides "factices" (sans aucun champ) pour imiter des arguments "optionnels" de génériques; en faisant des tests avec différentes dispositions impliquant des structures vides, je peux voir qu'une structure vide utilise ( au moins) 1 octet par élément; je me souviens vaguement que c'est parce que .Net a besoin d'une adresse différente pour chaque champ, ce qui ne fonctionnerait pas si un champ était vraiment vide / de taille 0).


0

Le moyen le plus simple est: int size = *((int*)type.TypeHandle.Value + 1)

Je sais que c'est un détail de mise en œuvre, mais GC s'appuie dessus et il doit être aussi proche du début de la table des méthodes pour plus d'efficacité, plus en tenant compte du fait que le code GC est complexe, personne n'osera le changer à l'avenir. En fait, cela fonctionne pour toutes les versions mineures / majeures de .net framework + .net core. (Actuellement impossible de tester pour 1.0)
Si vous voulez un moyen plus fiable, émettez une structure dans un assembly dynamique avec [StructLayout(LayoutKind.Auto)]avec exactement les mêmes champs dans le même ordre, prenez sa taille avec l' instruction sizeof IL. Vous souhaiterez peut-être émettre une méthode statique dans struct qui renvoie simplement cette valeur. Ajoutez ensuite 2 * IntPtr.Size pour l'en-tête d'objet. Cela devrait vous donner une valeur exacte.
Mais si votre classe dérive d'une autre classe, vous devez trouver chaque taille de classe de base séparément et les ajouter à nouveau + 2 * Inptr.Size pour l'en-tête. Vous pouvez le faire en obtenant des champs avec BindingFlags.DeclaredOnlyindicateur.
Les tableaux et les chaînes ajoutent simplement cette taille à sa longueur * taille de l'élément. Pour la taille cumulative des objets agrégés, vous devez mettre en œuvre une solution plus sophistiquée qui consiste à visiter chaque champ et à inspecter son contenu.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.