Dans Noda Time v2, nous passons à une résolution nanoseconde. Cela signifie que nous ne pouvons plus utiliser un entier de 8 octets pour représenter toute la plage de temps qui nous intéresse. Cela m'a incité à étudier l'utilisation de la mémoire des (nombreuses) structures de Noda Time, ce qui m'a conduit pour découvrir une légère bizarrerie dans la décision d'alignement du CLR.
Tout d'abord, je me rends compte qu'il s'agit d' une décision de mise en œuvre et que le comportement par défaut peut changer à tout moment. Je me rends compte que je peux le modifier en utilisant [StructLayout]
et [FieldOffset]
, mais je préfère trouver une solution qui ne l'exige pas si possible.
Mon scénario principal est que j'ai un struct
qui contient un champ de type référence et deux autres champs de type valeur, où ces champs sont de simples wrappers pour int
. J'avais espéré que cela serait représenté par 16 octets sur le CLR 64 bits (8 pour la référence et 4 pour chacun des autres), mais pour une raison quelconque, il utilise 24 octets. Je mesure l'espace à l'aide de tableaux, au fait - je comprends que la disposition peut être différente dans différentes situations, mais cela me semblait être un point de départ raisonnable.
Voici un exemple de programme illustrant le problème:
using System;
using System.Runtime.InteropServices;
#pragma warning disable 0169
struct Int32Wrapper
{
int x;
}
struct TwoInt32s
{
int x, y;
}
struct TwoInt32Wrappers
{
Int32Wrapper x, y;
}
struct RefAndTwoInt32s
{
string text;
int x, y;
}
struct RefAndTwoInt32Wrappers
{
string text;
Int32Wrapper x, y;
}
class Test
{
static void Main()
{
Console.WriteLine("Environment: CLR {0} on {1} ({2})",
Environment.Version,
Environment.OSVersion,
Environment.Is64BitProcess ? "64 bit" : "32 bit");
ShowSize<Int32Wrapper>();
ShowSize<TwoInt32s>();
ShowSize<TwoInt32Wrappers>();
ShowSize<RefAndTwoInt32s>();
ShowSize<RefAndTwoInt32Wrappers>();
}
static void ShowSize<T>()
{
long before = GC.GetTotalMemory(true);
T[] array = new T[100000];
long after = GC.GetTotalMemory(true);
Console.WriteLine("{0}: {1}", typeof(T),
(after - before) / array.Length);
}
}
Et la compilation et la sortie sur mon ordinateur portable:
c:\Users\Jon\Test>csc /debug- /o+ ShowMemory.cs
Microsoft (R) Visual C# Compiler version 12.0.30501.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.
c:\Users\Jon\Test>ShowMemory.exe
Environment: CLR 4.0.30319.34014 on Microsoft Windows NT 6.2.9200.0 (64 bit)
Int32Wrapper: 4
TwoInt32s: 8
TwoInt32Wrappers: 8
RefAndTwoInt32s: 16
RefAndTwoInt32Wrappers: 24
Alors:
- Si vous n'avez pas de champ de type de référence, le CLR est heureux de
Int32Wrapper
regrouper les champs (TwoInt32Wrappers
a une taille de 8) - Même avec un champ de type de référence, le CLR est toujours heureux d'emballer
int
regrouper les champs (RefAndTwoInt32s
a une taille de 16) - En combinant les deux, chaque
Int32Wrapper
champ semble être rempli / aligné sur 8 octets. (RefAndTwoInt32Wrappers
a une taille de 24.) - L'exécution du même code dans le débogueur (mais toujours une version de version) affiche une taille de 12.
Quelques autres expériences ont donné des résultats similaires:
- Mettre le champ de type de référence après les champs de type de valeur n'aide pas
- Utiliser
object
au lieu destring
n'aide pas (je suppose que c'est "n'importe quel type de référence") - Utiliser une autre structure comme "wrapper" autour de la référence n'aide pas
- Utiliser une structure générique comme wrapper autour de la référence n'aide pas
- Si je continue d'ajouter des champs (par paires pour plus de simplicité), les
int
champs comptent toujours pour 4 octets, etInt32Wrapper
champs comptent pour 8 octets - L'ajout
[StructLayout(LayoutKind.Sequential, Pack = 4)]
à chaque structure en vue ne change pas les résultats
Quelqu'un a-t-il une explication à ce sujet (idéalement avec une documentation de référence) ou une suggestion sur la façon dont je peux obtenir un indice au CLR que je voudrais que les champs soient compilés sans spécifier un décalage de champ constant?
TwoInt32Wrappers
, ou un Int64
et un TwoInt32Wrappers
? Et si vous créez un générique Pair<T1,T2> {public T1 f1; public T2 f2;}
puis créez Pair<string,Pair<int,int>>
et Pair<string,Pair<Int32Wrapper,Int32Wrapper>>
? Quelles combinaisons obligent le JITter à rembourrer les choses?
Pair<string, TwoInt32Wrappers>
ne donne que 16 octets, ce qui résoudrait le problème. Fascinant.
Marshal.SizeOf
renverra la taille de la structure qui serait passée au code natif, qui ne doit avoir aucune relation avec la taille de la structure dans le code .NET.
Ref<T>
mais utilisez à lastring
place, non pas que cela devrait faire une différence.