No Noda Time v2, estamos mudando para a resolução em nanossegundos. Isso significa que não podemos mais usar um número inteiro de 8 bytes para representar todo o intervalo de tempo em que estamos interessados. Isso me levou a investigar o uso de memória das (muitas) estruturas do Noda Time, o que, por sua vez, me levou para descobrir uma pequena estranheza na decisão de alinhamento do CLR.
Em primeiro lugar, percebo que essa é uma decisão de implementação e que o comportamento padrão pode mudar a qualquer momento. Sei que posso modificá-lo usando [StructLayout]
e [FieldOffset]
, mas prefiro criar uma solução que não exija isso, se possível.
Meu cenário principal é que eu tenho um struct
que contém um campo de tipo de referência e dois outros campos de tipo de valor, nos quais esses campos são invólucros simples int
. Eu esperava que isso fosse representado como 16 bytes no CLR de 64 bits (8 para a referência e 4 para cada um dos outros), mas por algum motivo, ele está usando 24 bytes. A propósito, estou medindo o espaço usando matrizes - entendo que o layout pode ser diferente em situações diferentes, mas isso parecia um ponto de partida razoável.
Aqui está um exemplo de programa demonstrando o problema:
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);
}
}
E a compilação e saída no meu laptop:
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
Assim:
- Se você não tiver um campo de tipo de referência, o CLR terá prazer em
Int32Wrapper
agrupar os campos (TwoInt32Wrappers
tem um tamanho de 8) - Mesmo com um campo de tipo de referência, o CLR ainda pode empacotar os
int
campos (RefAndTwoInt32s
tem um tamanho de 16) - Combinando os dois, cada
Int32Wrapper
campo parece estar preenchido / alinhado a 8 bytes. (RefAndTwoInt32Wrappers
tem um tamanho de 24.) - A execução do mesmo código no depurador (mas ainda uma versão compilada) mostra um tamanho de 12.
Alguns outros experimentos produziram resultados semelhantes:
- Colocar o campo do tipo de referência após os campos do tipo de valor não ajuda
- Usar em
object
vez destring
não ajuda (espero que seja "qualquer tipo de referência") - Usar outra estrutura como um "wrapper" em torno da referência não ajuda
- Usar uma estrutura genérica como invólucro em torno da referência não ajuda
- Se eu continuar adicionando campos (em pares por simplicidade), os
int
campos ainda contam com 4 bytes e osInt32Wrapper
campos contam com 8 bytes - Adicionar
[StructLayout(LayoutKind.Sequential, Pack = 4)]
a todas as estruturas à vista não altera os resultados
Alguém tem alguma explicação para isso (idealmente com documentação de referência) ou uma sugestão de como eu posso sugerir ao CLR que gostaria que os campos fossem compactados sem especificar um deslocamento de campo constante?
TwoInt32Wrappers
, ou um Int64
e um TwoInt32Wrappers
? Que tal se você criar um genérico Pair<T1,T2> {public T1 f1; public T2 f2;}
e depois criar Pair<string,Pair<int,int>>
e Pair<string,Pair<Int32Wrapper,Int32Wrapper>>
? Quais combinações forçam o JITter a preencher as coisas?
Pair<string, TwoInt32Wrappers>
não dar apenas 16 bytes, de modo que iria resolver o problema. Fascinante.
Marshal.SizeOf
retornará o tamanho da estrutura que seria passada ao código nativo, que não precisa ter nenhuma relação com o tamanho da estrutura no código .NET.
Ref<T>
mas está usandostring
, não que isso faça diferença.