Question

J'essayais de déterminer la surcharge de l'en-tête sur un tableau .NET (dans un processus 32 bits) en utilisant ce code :

long bytes1 = GC.GetTotalMemory(false);
object[] array = new object[10000];
    for (int i = 0; i < 10000; i++)
        array[i] = new int[1];
long bytes2 = GC.GetTotalMemory(false);
array[0] = null; // ensure no garbage collection before this point

Console.WriteLine(bytes2 - bytes1);
// Calculate array overhead in bytes by subtracting the size of 
// the array elements (40000 for object[10000] and 4 for each 
// array), and dividing by the number of arrays (10001)
Console.WriteLine("Array overhead: {0:0.000}", 
                  ((double)(bytes2 - bytes1) - 40000) / 10001 - 4);
Console.Write("Press any key to continue...");
Console.ReadKey();

Le résultat fut

    204800
    Array overhead: 12.478

Dans un processus 32 bits, object[1] devrait avoir la même taille que int[1], mais en fait, la surcharge augmente de 3,28 octets pour atteindre

    237568
    Array overhead: 15.755

Quelqu'un sait pourquoi ?

(Au fait, si quelqu'un est curieux, les frais généraux pour les objets non-tableaux, par ex.(objet)i dans la boucle ci-dessus fait environ 8 octets (8,384).J'ai entendu dire que c'était 16 octets dans les processus 64 bits.)

Était-ce utile?

La solution

Voici un programme court mais complet légèrement plus soigné (OMI) pour démontrer la même chose :

using System;

class Test
{
    const int Size = 100000;

    static void Main()
    {
        object[] array = new object[Size];
        long initialMemory = GC.GetTotalMemory(true);
        for (int i = 0; i < Size; i++)
        {
            array[i] = new string[0];
        }
        long finalMemory = GC.GetTotalMemory(true);
        GC.KeepAlive(array);

        long total = finalMemory - initialMemory;

        Console.WriteLine("Size of each element: {0:0.000} bytes",
                          ((double)total) / Size);
    }
}

Mais j'obtiens les mêmes résultats - la surcharge pour tout tableau de type référence est de 16 octets, alors que la surcharge pour tout tableau de type valeur est de 12 octets.J'essaie toujours de comprendre pourquoi, avec l'aide de la spécification CLI.N'oubliez pas que les tableaux de type référence sont covariants, ce qui peut être pertinent...

MODIFIER:Avec l'aide de cordbg, je peux confirmer la réponse de Brian : le pointeur de type d'un tableau de type référence est le même quel que soit le type d'élément réel.Vraisemblablement, il y a un peu de funky dans object.GetType() (qui n'est pas virtuel, rappelez-vous) pour en tenir compte.

Donc, avec le code de :

object[] x = new object[1];
string[] y = new string[1];
int[] z = new int[1];
z[0] = 0x12345678;
lock(z) {}

On se retrouve avec quelque chose comme ceci :

Variables:
x=(0x1f228c8) <System.Object[]>
y=(0x1f228dc) <System.String[]>
z=(0x1f228f0) <System.Int32[]>

Memory:
0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x
0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y
0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z

Notez que j'ai vidé la mémoire 1 mot avant la valeur de la variable elle-même.

Pour x et y, les valeurs sont :

  • Le bloc de synchronisation, utilisé pour verrouiller le code de hachage (ou un serrure fine - voir le commentaire de Brian)
  • Pointeur de saisie
  • Taille du tableau
  • Pointeur de type d'élément
  • Référence nulle (premier élément)

Pour z, les valeurs sont :

  • Bloc de synchronisation
  • Pointeur de saisie
  • Taille du tableau
  • 0x12345678 (premier élément)

Différents tableaux de type valeur (byte[], int[] etc.) se retrouvent avec des pointeurs de type différents, alors que tous les tableaux de type référence utilisent le même pointeur de type, mais ont un pointeur de type d'élément différent.Le pointeur de type d’élément a la même valeur que celle que vous trouveriez comme pointeur de type pour un objet de ce type.Ainsi, si nous examinions la mémoire d'un objet chaîne lors de l'exécution ci-dessus, il aurait un pointeur de type de 0x00329134.

Le mot avant le pointeur de type a certainement quelque chose à voir avec le moniteur ou le code de hachage :appel GetHashCode() remplit ce peu de mémoire, et je crois que la valeur par défaut object.GetHashCode() obtient un bloc de synchronisation pour garantir l'unicité du code de hachage pendant toute la durée de vie de l'objet.Cependant, il suffit de faire lock(x){} je n'ai rien fait, ce qui m'a surpris...

Soit dit en passant, tout cela n'est valable que pour les types "vecteur" - dans le CLR, un type "vecteur" est un tableau unidimensionnel avec une limite inférieure de 0.D'autres tableaux auront une disposition différente - d'une part, ils auraient besoin de stocker la limite inférieure...

Jusqu'à présent, il s'agissait d'expérimentations, mais voici des conjectures : la raison pour laquelle le système a été mis en œuvre de cette manière.À partir de maintenant, je ne fais que deviner.

  • Tous object[] les tableaux peuvent partager le même code JIT.Ils vont se comporter de la même manière en termes d'allocation de mémoire, d'accès aux tableaux, Length propriété et (surtout) la disposition des références pour le GC.Comparez cela avec des tableaux de types de valeur, où différents types de valeur peuvent avoir différentes « empreintes » GC (par ex.certains peuvent avoir un octet puis une référence, d'autres n'auront aucune référence, etc.).
  • Chaque fois que vous attribuez une valeur dans un object[] le runtime doit vérifier qu'il est valide.Il doit vérifier que le type de l'objet dont vous utilisez la référence pour la nouvelle valeur de l'élément est compatible avec le type d'élément du tableau.Par exemple:

    object[] x = new object[1];
    object[] y = new string[1];
    x[0] = new object(); // Valid
    y[0] = new object(); // Invalid - will throw an exception
    

C'est la covariance que j'ai mentionnée plus tôt.Maintenant, étant donné que cela va se produire pendant chaque mission, il est logique de réduire le nombre d'indirections.En particulier, je suppose que vous ne voulez pas vraiment faire exploser le cache en devant accéder à l'objet type pour chaque affectation pour obtenir le type d'élément.je suspect (et mon assemblage x86 n'est pas assez bon pour vérifier cela) que le test ressemble à ceci :

  • La valeur à copier est-elle une référence nulle ?Si c'est le cas, c'est très bien.(Fait.)
  • Récupère le pointeur de type de l’objet sur lequel pointe la référence.
  • Ce pointeur de type est-il le même que le pointeur de type d'élément (simple contrôle d'égalité binaire) ?Si c'est le cas, c'est très bien.(Fait.)
  • Cette affectation de pointeur de type est-elle compatible avec le pointeur de type d'élément ?(Contrôle beaucoup plus compliqué, avec héritage et interfaces impliqués.) Si c'est le cas, c'est très bien - sinon, lancez une exception.

Si nous pouvons terminer la recherche au cours des trois premières étapes, il n'y a pas beaucoup d'indirection - ce qui est bon pour quelque chose qui va se produire aussi souvent que les affectations de tableaux.Rien de tout cela ne doit se produire pour les affectations de types de valeur, car cela est vérifiable statiquement.

C'est pourquoi je pense que les tableaux de type référence sont légèrement plus grands que les tableaux de type valeur.

Excellente question - vraiment intéressant d'y plonger :)

Autres conseils

tableau est un type de référence. Tous les types de référence portent deux champs de mots supplémentaires. La référence de type et un champ d'index de SyncBlock, qui entre autres est utilisé pour implémenter des verrous dans le CLR. Donc, les frais généraux de type sur les types de référence est de 8 octets sur 32 bits. En plus de ce que le tableau lui-même stocke également la longueur qui est encore 4 octets. Cela porte le total des coûts indirects à 12 octets.

Je viens d'apprendre de la réponse de Jon Skeet, les tableaux de types de référence a un 4 octets frais généraux supplémentaires. Cela peut être confirmé en utilisant WinDbg. Il se trouve que le mot supplémentaire est une autre référence de type pour le type stocké dans le tableau. Tous les tableaux de types de référence sont stockés en interne comme object[], avec la référence supplémentaire à l'objet de type de type réel. Ainsi, un string[] est vraiment juste un object[] avec une référence de type supplémentaire au string type. Pour plus de détails s'il vous plaît voir ci-dessous.

valeurs stockées dans les tableaux: tableaux de types de référence tiennent références à des objets, de sorte que chaque entrée dans le tableau est la taille d'une référence (à savoir 4 octets sur 32 bits). Les tableaux de types de valeur stockent les valeurs en ligne et donc chaque élément prendra la taille du type en question.

Cette question peut aussi être d'intérêt: C # Liste size vs double taille []

Gory Détails

Considérez le code suivant

var strings = new string[1];
var ints = new int[1];

strings[0] = "hello world";
ints[0] = 42;

Fixation WinDbg affiche les éléments suivants:

Tout d'abord, nous allons jeter un coup d'oeil au tableau de type de valeur.

0:000> !dumparray -details 017e2acc 
Name: System.Int32[]
MethodTable: 63b9aa40
EEClass: 6395b4d4
Size: 16(0x10) bytes
Array: Rank 1, Number of elements 1, Type Int32
Element Methodtable: 63b9aaf0
[0] 017e2ad4
    Name: System.Int32
    MethodTable 63b9aaf0
    EEClass: 6395b548
    Size: 12(0xc) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    63b9aaf0  40003f0        0         System.Int32  1 instance       42 m_value <=== Our value

0:000> !objsize 017e2acc 
sizeof(017e2acc) =           16 (        0x10) bytes (System.Int32[])

0:000> dd 017e2acc -0x4
017e2ac8  00000000 63b9aa40 00000001 0000002a <=== That's the value

Tout d'abord nous vider le tableau et l'élément dont une avec valeur de 42. Comme on peut le voir la taille est de 16 octets. Soit 4 octets pour la valeur de int32 lui-même, de 8 octets pour la tête régulière du type de référence et un autre 4 octets pour la longueur de la matrice.

Le vidage brut montre la SyncBlock, la table de procédé pour int[], la longueur, et la valeur de 42 (2a en hexadécimal). Notez que le SyncBlock est situé juste en face de la référence d'objet.

Ensuite, nous regardons le string[] pour savoir ce que le mot supplémentaire est utilisé.

0:000> !dumparray -details 017e2ab8 
Name: System.String[]
MethodTable: 63b74ed0
EEClass: 6395a8a0
Size: 20(0x14) bytes
Array: Rank 1, Number of elements 1, Type CLASS
Element Methodtable: 63b988a4
[0] 017e2a90
    Name: System.String
    MethodTable: 63b988a4
    EEClass: 6395a498
    Size: 40(0x28) bytes <=== Size of the string
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     hello world    
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    63b9aaf0  4000096        4         System.Int32  1 instance       12 m_arrayLength
    63b9aaf0  4000097        8         System.Int32  1 instance       11 m_stringLength
    63b99584  4000098        c          System.Char  1 instance       68 m_firstChar
    63b988a4  4000099       10        System.String  0   shared   static Empty
    >> Domain:Value  00226438:017e1198 <<
    63b994d4  400009a       14        System.Char[]  0   shared   static WhitespaceChars
    >> Domain:Value  00226438:017e1760 <<

0:000> !objsize 017e2ab8 
sizeof(017e2ab8) =           60 (        0x3c) bytes (System.Object[]) <=== Notice the underlying type of the string[]

0:000> dd 017e2ab8 -0x4
017e2ab4  00000000 63b74ed0 00000001 63b988a4 <=== Method table for string
017e2ac4  017e2a90 <=== Address of the string in memory

0:000> !dumpmt 63b988a4
EEClass: 6395a498
Module: 63931000
Name: System.String
mdToken: 02000024  (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 7
Slots in VTable: 196

Tout d'abord nous vider le tableau et la chaîne. Ensuite, nous vidons la taille du string[]. Notez que WinDbg indique le type comme System.Object[] ici. La taille de l'objet dans ce cas comprend la chaîne elle-même, de sorte que la taille totale est le 20 partir de la matrice ainsi que le 40 pour la chaîne.

En déversant les octets bruts de l'instance, nous pouvons voir ce qui suit: Tout d'abord, nous avons le SyncBlock, suit alors la table de méthode pour object[], la longueur du tableau. Après cela, nous trouvons les 4 octets supplémentaires avec la référence à la table de la méthode de chaîne. Cela peut être vérifié par la commande dumpmt comme indiqué ci-dessus. Enfin, nous trouvons la seule référence à l'instance de chaîne réelle.

En conclusion

Les frais généraux pour les tableaux se décompose de la manière suivante (sur 32 bits qui est)

  • 4 octets SyncBlock
  • 4 octets pour la table de référence (méthode de type) pour le tableau lui-même
  • 4 octets pour la durée du tableau
  • tableaux de types de référence ajoute encore 4 octets pour tenir le tableau de procédé du type réel de l'élément (réseaux du type de référence sont object[] sous le capot)

i.e.. la tête est 12 octets pour des réseaux de type valeur et 16 octets pour des réseaux de type référence .

Je pense que vous faites des suppositions erronées tout en mesurant, comme l'allocation de mémoire (via GetTotalMemory) au cours de votre boucle peut être différente de la mémoire requise réelle uniquement pour les tableaux - la mémoire peut être allouée dans des blocs plus importants, il peut y avoir d'autres objets en mémoire qui sont récupérés au cours de la boucle, etc.

Voici quelques informations pour vous sur les frais généraux de tableau:

Parce que la gestion du tas (puisque vous traitez avec GetTotalMemory) ne peut allouer des blocs assez grandes, ces derniers étant attribués par petits morceaux à des fins de programmeur par CLR.

Je suis désolé pour le hors-sujet mais j'ai trouvé informations intéressantes sur la mémoire juste matin verticale d'aujourd'hui.

Nous avons un projet qui fonctionne énorme quantité de données (jusqu'à 2 Go). Comme le stockage important que nous utilisons Dictionary<T,T>. Des milliers de dictionnaires sont créés en fait. changer après à List<T> pour les clés et List<T> pour les valeurs (nous nous IDictionary<T,T> mis en œuvre) l'utilisation de la mémoire a diminué à environ 30-40%.

Pourquoi?

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top