Pergunta

Eu estava tentando determinar a sobrecarga do cabeçalho em uma matriz .NET (em um processo de 32 bits) usando este código:

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

O resultado foi

    204800
    Array overhead: 12.478

Em um processo de 32 bits, o objeto [1] deve ter o mesmo tamanho que o int [1], mas de fato o salto aéreo salta 3,28 bytes para

    237568
    Array overhead: 15.755

Alguém sabe por quê?

(A propósito, se alguém estiver curioso, a sobrecarga para objetos que não são de marca, por exemplo (objeto) I no loop acima, é de cerca de 8 bytes (8.384). Ouvi dizer que são 16 bytes em processos de 64 bits.)

Foi útil?

Solução

Aqui está um programa um pouco mais intenso (IMO), mas completo, para demonstrar a mesma coisa:

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

Mas recebo os mesmos resultados - a sobrecarga para qualquer matriz de tipo de referência é de 16 bytes, enquanto a sobrecarga para qualquer matriz de tipo de valor é de 12 bytes. Ainda estou tentando descobrir por que isso é, com a ajuda das especificações da CLI. Não se esqueça que as matrizes do tipo de referência são covariantes, o que pode ser relevante ...

EDIT: Com a ajuda do cordbg, posso confirmar a resposta de Brian - o ponteiro do tipo de uma matriz do tipo referência é o mesmo, independentemente do tipo de elemento real. Presumivelmente há alguma funkiness em object.GetType() (que não é virtual, lembre-se) de explicar isso.

Então, com código de:

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

Acabamos com algo como o seguinte:

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

Observe que eu larguei a memória 1 palavra antes da o valor da própria variável.

Por x e y, os valores são:

  • O bloco de sincronização, usado para travar o código de hash (ou um trava fina - Veja o comentário de Brian)
  • Digite ponteiro
  • Tamanho da matriz
  • Ponteiro do tipo de elemento
  • Referência nula (primeiro elemento)

Por z, os valores são:

  • Bloqueio de sincronização
  • Digite ponteiro
  • Tamanho da matriz
  • 0x12345678 (primeiro elemento)

Diferentes matrizes de tipo de valor (byte [], int [] etc) acabam com diferentes ponteiros de tipo, enquanto todas as matrizes do tipo de referência usam o mesmo ponteiro de tipo, mas têm um ponteiro de tipo de elemento diferente. O ponteiro do tipo elemento é o mesmo valor que você encontraria como o ponteiro de tipo para um objeto desse tipo. Portanto, se analisássemos a memória de um objeto de string na execução acima, ele teria um ponteiro de tipo de 0x00329134.

A palavra antes do ponteiro do tipo certamente tem algo Para fazer com o monitor ou o código de hash: chamando GetHashCode() preenche esse pedaço de memória, e acredito que o padrão object.GetHashCode() obtém um bloco de sincronização para garantir a singularidade do código de hash durante a vida útil do objeto. No entanto, apenas fazendo lock(x){} não fiz nada, o que me surpreendeu ...

Tudo isso é válido apenas para tipos de "vetor", a propósito - no CLR, um tipo "vetor" é uma matriz única dimensional com um limite inferior de 0. Outras matrizes terão um layout diferente - para uma coisa , eles precisariam do limite inferior armazenado ...

Até agora, isso tem sido experimentação, mas aqui está a adivinhação - a razão para o sistema ser implementado da maneira que ele tem. A partir daqui, eu realmente estou apenas adivinhando.

  • Tudo object[] As matrizes podem compartilhar o mesmo código JIT. Eles vão se comportar da mesma maneira em termos de alocação de memória, acesso à matriz, Length propriedade e (importante) o layout das referências para o GC. Compare isso com as matrizes do tipo de valor, onde diferentes tipos de valor podem ter diferentes "pegadas" GC (por exemplo, alguém pode ter um byte e, em seguida, uma referência, outros não terão referências, etc.).
  • Toda vez que você atribui um valor dentro de um object[] O tempo de execução precisa verificar se é válido. Ele precisa verificar se o tipo de objeto cuja referência você está usando para o novo valor do elemento é compatível com o tipo de elemento da matriz. Por exemplo:

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

Esta é a covariância que mencionei anteriormente. Agora, dado que isso vai acontecer para cada tarefa, faz sentido reduzir o número de indiramentos. Em particular, suspeito que você realmente não queira explodir o cache, tendo que ir ao objeto Type para cada Assigment para obter o tipo de elemento. EU suspeito (E minha montagem x86 não é boa o suficiente para verificar isso) que o teste é algo como:

  • O valor a ser copiado é uma referência nula? Nesse caso, tudo bem. (Feito.)
  • Pegue o ponteiro de tipo do objeto que os pontos de referência estão em.
  • Esse ponteiro do tipo é o mesmo que o ponteiro do tipo elemento (verificação simples da igualdade binária)? Nesse caso, tudo bem. (Feito.)
  • Esse tipo é compatível com a atribuição de ponteiro com o ponteiro do tipo de elemento? (Verificação muito mais complicada, com herança e interfaces envolvidas.) Se sim, tudo bem - caso contrário, faça uma exceção.

Se pudermos encerrar a pesquisa nas três primeiras etapas, não há muita indireção - o que é bom para algo que acontecerá com tanta frequência quanto as atribuições de matriz. Nada disso precisa acontecer para atribuições de tipo de valor, porque isso é verificável estaticamente.

Então, é por isso que acredito que as matrizes de tipo de referência são um pouco maiores que as matrizes do tipo de valor.

Ótima pergunta - realmente interessante para mergulhar nisso :)

Outras dicas

Array é um tipo de referência. Todos os tipos de referência carregam dois campos de palavras adicionais. A referência de tipo e um campo de índice de sinclock, que, entre outras coisas, é usado para implementar bloqueios no CLR. Portanto, a sobrecarga do tipo nos tipos de referência é de 8 bytes em 32 bits. Além disso, a própria matriz também armazena o comprimento que é outros 4 bytes. Isso eleva a sobrecarga total para 12 bytes.

E eu apenas aprendi com a resposta de Jon Skeet, as matrizes de tipos de referência têm 4 bytes adicionais. Isso pode ser confirmado usando o WindBG. Acontece que a palavra adicional é outra referência de tipo para o tipo armazenado na matriz. Todas as matrizes de tipos de referência são armazenadas internamente como object[], com a referência adicional ao objeto de tipo do tipo real. Então a string[] é realmente apenas um object[] com um tipo adicional de referência ao tipo string. Para detalhes, veja abaixo.

Valores armazenados em matrizes: Matrizes de tipos de referência mantêm referências a objetos; portanto, cada entrada na matriz é o tamanho de uma referência (ou seja, 4 bytes em 32 bits). Matrizes de tipos de valor armazenam os valores embutidos e, portanto, cada elemento assumirá o tamanho do tipo em questão.

Esta questão também pode ser de interesse: C# Listau003Cdouble> tamanho vs duplo [] tamanho

Detalhes sangrentos

Considere o seguinte código

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

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

Anexar o Windbg mostra o seguinte:

Primeiro, vamos dar uma olhada na matriz do tipo de valor.

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

Primeiro, despejamos a matriz e o elemento único com valor de 42. Como pode ser visto, o tamanho é de 16 bytes. São 4 bytes para o int32 O próprio valor, 8 bytes para sobrecarga de referência regular e outros 4 bytes para o comprimento da matriz.

O despejo bruto mostra o Syncblock, a tabela de métodos para int[], o comprimento e o valor de 42 (2a em hexadecimal). Observe que o SyncBlock está localizado logo em frente à referência do objeto.

Em seguida, vejamos o string[] Para descobrir para que serve a palavra adicional.

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

Primeiro, despejamos a matriz e a corda. Em seguida, despejamos o tamanho do string[]. Observe que o Windbg lista o tipo como System.Object[] aqui. O tamanho do objeto neste caso inclui a própria string, portanto o tamanho total é o 20 da matriz mais o 40 para a string.

Ao despejar os bytes crus da instância, podemos ver o seguinte: Primeiro, temos o Syncblock, depois segue a tabela de métodos para object[], então o comprimento da matriz. Depois disso, encontramos os 4 bytes adicionais com a referência à tabela de métodos para string. Isso pode ser verificado pelo comando dumpmt, como mostrado acima. Finalmente, encontramos a única referência à instância real da string.

Para concluir

A sobrecarga para matrizes pode ser quebrada da seguinte maneira (em 32 bits)

  • 4 Bytes Syncblock
  • 4 bytes para tabela de método (referência de tipo) para a própria matriz
  • 4 bytes para o comprimento da matriz
  • Matrizes de tipos de referência adicionam outros 4 bytes para manter a tabela de métodos do tipo de elemento real (as matrizes do tipo de referência são object[] sob o capô)

Ou seja, a sobrecarga é 12 bytes para matrizes de tipo de valor e 16 bytes para matrizes de tipo de referência.

Eu acho que você está fazendo algumas suposições defeituosas durante a medição, como a alocação de memória (via getToTalmemory) durante o seu loop pode ser diferente da memória necessária real apenas para as matrizes - a memória pode ser alocada em blocos maiores, pode haver outros objetos em Memória que é recuperada durante o loop, etc.

Aqui estão algumas informações para você na Array Overhead:

Como o gerenciamento de heap (já que você lida com o GettoTalMemory) pode alocar apenas blocos bastante grandes, que os últimos são alocados por pedaços menores para fins de programador por CLR.

Sinto muito pelo offtopic, mas encontrei informações interessantes sobre a sobrecarga de memória hoje de hoje.

Temos um projeto que opera enorme quantidade de dados (até 2 GB). Como o principal armazenamento que usamos Dictionary<T,T>. Na verdade, milhares de dicionários são criados. Depois de mudar para List<T> para chaves e List<T> Para valores (implementamos IDictionary<T,T> nós mesmos) o uso da memória diminuiu em cerca de 30-40%.

Por quê?

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top