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?
Ref<T>
mas está usandostring
, não que isso faça diferença.TwoInt32Wrappers
, ou umInt64
e umTwoInt32Wrappers
? Que tal se você criar um genéricoPair<T1,T2> {public T1 f1; public T2 f2;}
e depois criarPair<string,Pair<int,int>>
ePair<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.Respostas:
Eu penso que isto é um erro. Você está vendo o efeito colateral do layout automático, ele gosta de alinhar campos não triviais a um endereço com múltiplos de 8 bytes no modo de 64 bits. Isso ocorre mesmo quando você aplica explicitamente o
[StructLayout(LayoutKind.Sequential)]
atributo. Isso não deveria acontecer.Você pode vê-lo tornando os membros struct públicos e anexando o código de teste como este:
Quando o ponto de interrupção atingir, use Debug + Windows + Memory + Memory 1. Alterne para números inteiros de 4 bytes e coloque
&test
no campo Endereço:0xe90ed750e0
é o ponteiro de string na minha máquina (não na sua). Você pode ver facilmenteInt32Wrappers
, com os 4 bytes extras de preenchimento que transformaram o tamanho em 24 bytes. Volte para a estrutura e coloque a string por último. Repita e você verá que o ponteiro da string ainda é o primeiro. ViolandoLayoutKind.Sequential
, você entendeuLayoutKind.Auto
.Vai ser difícil convencer a Microsoft a consertar isso, pois funcionou dessa maneira por muito tempo, portanto qualquer alteração quebrará alguma coisa . O CLR apenas tenta honrar
[StructLayout]
a versão gerenciada de uma estrutura e torná-la blittable, geralmente desiste rapidamente. Notoriamente para qualquer estrutura que contenha um DateTime. Você obtém a verdadeira garantia LayoutKind ao organizar uma estrutura. A versão empacotada certamente tem 16 bytes, comoMarshal.SizeOf()
será o caso.O uso
LayoutKind.Explicit
corrige, não o que você queria ouvir.fonte
string
por outro novo tipo de referência (class
) ao qual se aplica[StructLayout(LayoutKind.Sequential)]
não parece mudar nada. Na direção oposta, aplicar[StructLayout(LayoutKind.Auto)]
àsstruct Int32Wrapper
alterações no uso de memóriaTwoInt32Wrappers
.EDIT2
Esse código será alinhado em 8 bytes, portanto a estrutura terá 16 bytes. Em comparação, isso:
Será alinhado em 4 bytes, portanto, essa estrutura também terá 16 bytes. Portanto, a lógica aqui é que o alinhamento de estrutura no CLR é determinado pelo número de campos mais alinhados, as classes obviamente não podem fazer isso, para que permaneçam alinhados por 8 bytes.
Agora, se combinarmos tudo isso e criar struct:
Ele terá 24 bytes {x, y} terá 4 bytes cada e {z, s} terá 8 bytes. Depois que introduzirmos um tipo ref na estrutura, o CLR sempre alinhará nossa estrutura personalizada para corresponder ao alinhamento da classe.
Esse código terá 24 bytes, pois o Int32Wrapper será alinhado da mesma forma que o comprimento. Portanto, o wrapper de estrutura personalizado sempre se alinhará com o campo mais alto / melhor alinhado na estrutura ou com seus próprios campos internos mais significativos. Portanto, no caso de uma string de referência com 8 bytes alinhados, o wrapper struct será alinhado a isso.
O campo struct personalizado final dentro de struct sempre será alinhado ao campo de instância alinhado mais alto da estrutura. Agora, se eu não tenho certeza se isso é um bug, mas sem algumas evidências, vou manter minha opinião de que isso pode ser uma decisão consciente.
EDITAR
Na verdade, os tamanhos são precisos apenas quando alocados em uma pilha, mas as estruturas em si têm tamanhos menores (os tamanhos exatos dos seus campos). Uma análise mais aprofundada sugere que isso pode ser um bug no código CLR, mas precisa ser apoiado por evidências.
Vou inspecionar o código cli e postar mais atualizações se algo útil for encontrado.
Essa é uma estratégia de alinhamento usada pelo alocador de memórias .NET.
Esse código compilado com .net40 em x64, no WinDbg, faça o seguinte:
Vamos encontrar o tipo no Heap primeiro:
Assim que tivermos, vamos ver o que está sob esse endereço:
Vemos que esse é um ValueType e foi o que criamos. Como esta é uma matriz, precisamos obter a definição ValueType de um único elemento na matriz:
A estrutura é na verdade de 32 bytes, já que seus 16 bytes são reservados para preenchimento; portanto, na realidade, toda estrutura possui pelo menos 16 bytes de tamanho desde o início.
se você adicionar 16 bytes de ints e uma string ref a: 0000000003e72d18 + 8 bytes EE / padding, você terminará em 0000000003e72d30 e este é o ponto de partida para a referência de string, e como todas as referências são preenchidas em 8 bytes no primeiro campo de dados real isso compensa nossos 32 bytes para essa estrutura.
Vamos ver se a string é realmente preenchida dessa maneira:
Agora vamos analisar o programa acima da mesma maneira:
Nossa estrutura é de 48 bytes agora.
Aqui a situação é a mesma: se adicionarmos a 0000000003c22d18 + 8 bytes da string ref, terminaremos no início do primeiro wrapper Int, onde o valor realmente aponta para o endereço em que estamos.
Agora podemos ver que cada valor é uma referência a objeto novamente e vamos confirmar que, espreitando 0000000003c22d20.
Na verdade, isso está correto, já que é uma estrutura, o endereço não diz nada se for um obj ou vt.
Portanto, na realidade, isso é mais parecido com um tipo Union que terá 8 bytes alinhados desta vez (todos os preenchimentos serão alinhados com a estrutura pai). Se não fosse, teríamos 20 bytes e isso não é o ideal, para que o alocador de memórias nunca permita que isso aconteça. Se você fizer as contas novamente, a estrutura terá realmente 40 bytes de tamanho.
Portanto, se você deseja ser mais conservador com a memória, nunca deve empacotá-la em um tipo de estrutura personalizada struct, mas usar matrizes simples. Outra maneira é alocar memória fora do heap (VirtualAllocEx, por exemplo), dessa forma, você recebe seu próprio bloco de memória e o gerencia da maneira que desejar.
A questão final aqui é por que, de repente, podemos ter um layout assim. Bem, se você comparar o código jited e o desempenho de uma incremento int [] com struct [] com um incremento de campo de contador, o segundo gerará um endereço alinhado de 8 bytes sendo uma união, mas quando jited isso se traduzirá em um código de montagem mais otimizado (singe LEA vs MOV múltiplo). No entanto, no caso descrito aqui, o desempenho será realmente pior, portanto, acredito que isso seja consistente com a implementação subjacente do CLR, já que é um tipo personalizado que pode ter vários campos; portanto, é mais fácil / melhor colocar o endereço inicial em vez de um value (já que seria impossível) e faça o preenchimento de estruturas, resultando em um tamanho maior de bytes.
fonte
RefAndTwoInt32Wrappers
não é 32 bytes - é 24, o mesmo que o relatado no meu código. Se você olhar na exibição de memória em vez de usardumparray
e procurar na memória uma matriz com (digamos) 3 elementos com valores distinguíveis, poderá ver claramente que cada elemento consiste em uma referência de cadeia de 8 bytes e dois números inteiros de 8 bytes . Eu suspeito quedumparray
está mostrando os valores como referências simplesmente porque não sabe como exibirInt32Wrapper
valores. Essas "referências" apontam para si mesmas; eles não são valores separados.dumparray
mostra.Int32
. Não estou muito preocupado com o que faz na pilha, para ser sincero - mas ainda não o verifiquei.Resumo, veja a resposta de @Hans Passant provavelmente acima. Sequencial de layout não funciona
Alguns testes:
Definitivamente, é apenas em 64 bits e o objeto de referência "envenena" a estrutura. 32 bits faz o que você está esperando:
Assim que a referência do objeto é adicionada, todas as estruturas se expandem para 8 bytes, em vez do tamanho de 4 bytes. Expandindo os testes:
Como você pode ver, assim que a referência é adicionada, todo Int32Wrapper se torna 8 bytes, portanto não é um alinhamento simples. Eu reduzi a alocação de matriz, pois era a alocação de LoH que está alinhada de maneira diferente.
fonte
Apenas para adicionar alguns dados à mistura - criei mais um tipo dos que você tinha:
O programa escreve:
Portanto, parece que a
TwoInt32Wrappers
estrutura está alinhada corretamente na novaRefAndTwoInt32Wrappers2
estrutura.fonte