Tipos de referência do .NET vs tipos de valor. Parte 1

Primeiro, vamos falar sobre tipos de referência e tipos de valor. Eu acho que as pessoas realmente não entendem as diferenças e os benefícios de ambos. Eles geralmente dizem que os tipos de referência armazenam conteúdo na pilha e os tipos de valor armazenam conteúdo na pilha, o que está errado.


Vamos discutir as diferenças reais:


  • Um tipo de valor : seu valor é uma estrutura inteira . O valor de um tipo de referência é uma referência a um objeto. - Uma estrutura na memória: os tipos de valor contêm apenas os dados que você indicou. Os tipos de referência também contêm dois campos do sistema. O primeiro armazena 'SyncBlockIndex', o segundo armazena as informações sobre um tipo, incluindo as informações sobre uma Tabela de métodos virtuais (VMT).
  • Os tipos de referência podem ter métodos que são substituídos quando herdados. Os tipos de valor não podem ser herdados.
  • Você deve alocar espaço no heap para uma instância de um tipo de referência. Um tipo de valor pode ser alocado na pilha ou ele se torna parte de um tipo de referência. Isso aumenta suficientemente o desempenho de alguns algoritmos.

No entanto, existem recursos comuns:


  • Ambas as subclasses podem herdar o tipo de objeto e se tornar seus representantes.

Vamos examinar mais de perto cada recurso.


Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.

Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório github / sidristij / dotnetbook .


Vamos examinar mais de perto cada recurso.


Copiando


A principal diferença entre os dois tipos é a seguinte:


  • Cada variável, campos de classe ou estrutura ou parâmetros de método que usam um tipo de referência armazenam uma referência a um valor;
  • Mas cada variável, classe ou campo de estrutura ou parâmetro de método que utiliza um tipo de valor armazena um valor exatamente, isto é, uma estrutura inteira.

Isso significa que atribuir ou passar um parâmetro para um método copiará o valor. Mesmo se você alterar a cópia, o original permanecerá o mesmo. No entanto, se você alterar os campos do tipo de referência, isso "afetará" todas as partes com uma referência a uma instância de um tipo. Vamos olhar para o
exemplo:


DateTime dt = DateTime.Now; // Here, we allocate space for DateTime variable when calling a method, // but it will contain zeros. Next, let's copy all // values of the Now property to dt variable DateTime dt2 = dt; // Here, we copy the value once again object obj = new object(); // Here, we create an object by allocating memory on the Small Object Heap, // and put a pointer to the object in obj variable object obj2 = obj; // Here, we copy a reference to this object. Finally, // we have one object and two references. 

Parece que essa propriedade produz construções de código ambíguas, como o
mudança de código nas coleções:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create an array of such structures and initialize the Data field = 5 var array = new [] { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field array[0].Data = 4; // Let's check the value Console.WriteLine(array[0].Data); 

Há um pequeno truque neste código. Parece que obtemos a instância da estrutura primeiro e depois atribuímos um novo valor ao campo Dados da cópia. Isso significa que devemos obter 5 novamente ao verificar o valor. No entanto, isso não acontece. O MSIL possui uma instrução separada para definir os valores dos campos nas estruturas de uma matriz, o que aumenta o desempenho. O código funcionará como pretendido: o programa
saída 4 para um console.


Vamos ver o que acontecerá se mudarmos este código:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field list[0].Data = 4; // Let's check the value Console.WriteLine(list[0].Data); 

A compilação deste código falhará, porque quando você escreve a list[0].Data = 4 você obtém a cópia da estrutura primeiro. Na verdade, você está chamando um método de instância do tipo List<T> subjacente ao acesso por um índice. Ele pega a cópia de uma estrutura de uma matriz interna (a List<T> armazena dados em matrizes) e retorna essa cópia para você do método de acesso usando um índice. Em seguida, você tenta modificar a cópia, que não é usada mais adiante. Este código é inútil. Um compilador proíbe esse comportamento, sabendo que as pessoas abusam dos tipos de valor. Devemos reescrever este exemplo da seguinte maneira:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field. Then, let's save it again. var copy = list[0]; copy.Data = 4; list[0] = copy; // Let's check the value Console.WriteLine(list[0].Data); 

Este código está correto, apesar de sua aparente redundância. O programa irá
saída 4 para um console.


O próximo exemplo mostra o que quero dizer com “o valor de uma estrutura é um
estrutura inteira "


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } int x = 5; PersonInfo person; int y = 6; // Variant 2 int x = 5; int Height; int Width; int HairColor; int y = 6; 

Ambos os exemplos são semelhantes em termos de localização de dados na memória, pois o valor da estrutura é toda a estrutura. Aloca a memória para si mesma onde está.


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } class Employee { public int x; public PersonInfo person; public int y; } // Variant 2 class Employee { public int x; public int Height; public int Width; public int HairColor; public int y; } 

Esses exemplos também são semelhantes em termos da localização dos elementos na memória, pois a estrutura ocupa um lugar definido entre os campos da classe. Eu não digo que eles são totalmente semelhantes, pois você pode operar campos de estrutura usando métodos de estrutura.


Obviamente, esse não é o caso dos tipos de referência. Uma instância em si está no Small Object Heap inacessível (SOH) ou no Large Object Heap (LOH). Um campo de classe contém apenas o valor de um ponteiro para uma instância: um número de 32 ou 64 bits.


Vejamos o último exemplo para fechar o problema.


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } void Method(int x, PersonInfo person, int y); // Variant 2 void Method(int x, int HairColor, int Width, int Height, int y); 

Em termos de memória, ambas as variantes de código funcionarão de maneira semelhante, mas não em termos de arquitetura. Não é apenas uma substituição de um número variável de argumentos. A ordem muda porque os parâmetros do método são declarados um após o outro. Eles são colocados na pilha da mesma maneira.


No entanto, a pilha cresce de endereços mais altos para mais baixos. Isso significa que a ordem de empurrar uma estrutura peça por peça será diferente de empurrá-la como um todo.


Métodos substituíveis e herança


A próxima grande diferença entre os dois tipos é a falta de virtual
tabela de métodos em estruturas. Isso significa que:


  1. Você não pode descrever e substituir métodos virtuais em estruturas.
  2. Uma estrutura não pode herdar outra. A única maneira de emular herança é colocar uma estrutura de tipo base no primeiro campo. Os campos de uma estrutura "herdada" irão atrás dos campos de uma estrutura "base" e criarão herança lógica. Os campos de ambas as estruturas coincidirão com base no deslocamento.
  3. Você pode passar estruturas para código não gerenciado. No entanto, você perderá as informações sobre métodos. Isso ocorre porque uma estrutura é apenas espaço na memória, preenchida com dados sem as informações sobre um tipo. Você pode passá-lo para métodos não gerenciados, por exemplo, escritos em C ++, sem alterações.

A falta de uma tabela de métodos virtuais subtrai uma certa parte da “mágica” de herança das estruturas, mas oferece outras vantagens. O primeiro é que podemos passar instâncias dessa estrutura para ambientes externos (fora do .NET Framework). Lembre-se, isso é apenas uma memória
alcance! Também podemos pegar um intervalo de memória de código não gerenciado e converter um tipo em nossa estrutura para tornar seus campos mais acessíveis. Você não pode fazer isso com as classes, pois elas têm dois campos inacessíveis. Estes são SyncBlockIndex e um endereço de tabela de métodos virtuais. Se esses dois campos passarem para código não gerenciado, será perigoso. Usando uma tabela de métodos virtuais, é possível acessar qualquer tipo e alterá-lo para atacar um aplicativo.


Vamos mostrar que é apenas um intervalo de memória sem lógica adicional.


 unsafe void Main() { int secret = 666; HeightHolder hh; hh.Height = 5; WidthHolder wh; unsafe { // This cast wouldn't work if structures had the information about a type. // The CLR would check a hierarchy before casting a type and if it didn't find WidthHolder, // it would output an InvalidCastException exception. But since a structure is a memory range, // you can interpret it as any kind of structure. wh = *(WidthHolder*)&hh; } Console.WriteLine("Width: " + wh.Width); Console.WriteLine("Secret:" + wh.Secret); } struct WidthHolder { public int Width; public int Secret; } struct HeightHolder { public int Height; } 

Aqui, realizamos a operação que é impossível na digitação forte. Lançamos um tipo para outro incompatível que contém um campo extra. Introduzimos uma variável adicional dentro do método Main. Em teoria, seu valor é secreto. No entanto, o código de exemplo produzirá o valor de uma variável, não encontrada em nenhuma das estruturas dentro do método Main() . Você pode considerar uma violação de segurança, mas as coisas não são tão simples. Você não pode se livrar do código não gerenciado em um programa. O principal motivo é a estrutura da pilha de threads. Pode-se usá-lo para acessar código não gerenciado e brincar com variáveis ​​locais. Você pode defender seu código desses ataques aleatoriamente o tamanho de um quadro de pilha. Ou, você pode excluir as informações sobre o registro EBP para complicar o retorno de um quadro de pilha. No entanto, isso não importa para nós agora. O que estamos interessados ​​neste exemplo é o seguinte. A variável "secreta" vai antes da definição da variável hh e depois na estrutura WidthHolder (em lugares diferentes, na verdade). Então, por que conseguimos facilmente esse valor? A resposta é que a pilha cresce da direita para a esquerda. As variáveis ​​declaradas primeiro terão endereços muito mais altos e as declaradas posteriormente terão endereços mais baixos.


O comportamento ao chamar métodos de instância


Ambos os tipos de dados têm outro recurso que não é fácil de ver e pode explicar a estrutura dos dois tipos. Ele lida com a chamada de métodos de instância.


 // The example with a reference type class FooClass { private int x; public void ChangeTo(int val) { x = val; } } // The example with a value type struct FooStruct { private int x; public void ChangeTo(int val) { x = val; } } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); klass.ChangeTo(10); strukt.ChangeTo(10); 

Logicamente, podemos decidir que o método possui um corpo compilado. Em outras palavras, não há instância de um tipo que tenha seu próprio conjunto compilado de métodos, semelhante aos conjuntos de outras instâncias. No entanto, o método chamado sabe a qual instância pertence, como referência à instância de um tipo é o primeiro parâmetro. Podemos reescrever nosso exemplo e será idêntico ao que dissemos antes. Não estou usando um exemplo deliberadamente com métodos virtuais, pois eles têm outro procedimento.


 // An example with a reference type class FooClass { public int x; } // An example with a value type struct FooStruct { public int x; } public void ChangeTo(FooClass klass, int val) { klass.x = val; } public void ChangeTo(ref FooStruct strukt, int val) { strukt.x = val; } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); ChangeTo(klass, 10); ChangeTo(ref strukt, 10); 

Eu devo explicar o uso da palavra-chave ref. Se não o usasse, obteria uma cópia da estrutura como parâmetro de método em vez do original. Então eu mudaria, mas o original continuaria o mesmo. Eu precisaria retornar uma cópia alterada de um método para um chamador (outra cópia), e o chamador salvaria esse valor novamente na variável (mais uma cópia). Em vez disso, um método de instância obtém um ponteiro e o utiliza para alterar o original imediatamente. O uso de um ponteiro não influencia o desempenho, pois qualquer operação no nível do processador usa ponteiros. Ref é uma parte do mundo C #, não mais.


A capacidade de apontar para a posição dos elementos.


As estruturas e as classes têm outra capacidade de apontar para o deslocamento de um campo específico em relação ao início de uma estrutura na memória. Isso serve a vários propósitos:


  • trabalhar com APIs externas no mundo não gerenciado sem precisar inserir campos não utilizados antes de um necessário;
  • para instruir um compilador a localizar um campo logo no início do tipo ( [FieldOffset(0)] ). Isso tornará o trabalho com esse tipo mais rápido. Se for um campo usado com frequência, podemos aumentar o desempenho do aplicativo. No entanto, isso é verdade apenas para tipos de valor. Nos tipos de referência, o campo com deslocamento zero contém o endereço de uma tabela de métodos virtuais, que leva 1 palavra de máquina. Mesmo se você endereçar o primeiro campo de uma classe, ele usará endereçamento complexo (endereço + deslocamento). Isso ocorre porque o campo de classe mais usado é o endereço de uma tabela de métodos virtuais. A tabela é necessária para chamar todos os métodos virtuais;
  • para apontar para vários campos usando um endereço. Nesse caso, o mesmo valor é interpretado como tipos de dados diferentes. Em C ++, esse tipo de dados é chamado de união;
  • para não se preocupar em declarar nada: um compilador alocará os campos de maneira ideal. Assim, a ordem final dos campos pode ser diferente.

Observações gerais


  • Automático : o ambiente de tempo de execução escolhe automaticamente um local e uma embalagem para todos os campos de classe ou estrutura. As estruturas definidas marcadas por um membro dessa enumeração não podem passar para o código não gerenciado. A tentativa de fazer isso produzirá uma exceção;
  • Explícito : um programador controla explicitamente a localização exata de cada campo de um tipo com o FieldOffsetAttribute;
  • Sequencial : os membros do tipo vêm em uma ordem sequencial, definida durante o design do tipo. O valor StructLayoutAttribute.Pack de uma etapa de empacotamento indica sua localização.

Usando FieldOffset para pular campos de estrutura não utilizados


As estruturas provenientes do mundo não gerenciado podem conter campos reservados. Pode-se usá-los em uma versão futura de uma biblioteca. Em C / C ++, preenchemos essas lacunas adicionando campos, por exemplo, reservados1, reservados2, ... No entanto, no .NET apenas compensamos o início de um campo usando o atributo FieldOffsetAttribute e [StructLayout(LayoutKind.Explicit)] .


 [StructLayout(LayoutKind.Explicit)] public struct SYSTEM_INFO { [FieldOffset(0)] public ulong OemId; // 92 bytes reserved [FieldOffset(100)] public ulong PageSize; [FieldOffset(108)] public ulong ActiveProcessorMask; [FieldOffset(116)] public ulong NumberOfProcessors; [FieldOffset(124)] public ulong ProcessorType; } 

Uma lacuna é ocupada, mas não é utilizada. A estrutura terá o tamanho igual a 132 e não 40 bytes, como pode parecer desde o início.


União


Usando o FieldOffsetAttribute, você pode emular o tipo C / C ++ chamado união. Permite acessar os mesmos dados que as entidades de
tipos diferentes. Vejamos o exemplo:


 // If we read the RGBA.Value, we will get an Int32 value accumulating all // other fields. // However, if we try to read the RGBA.R, RGBA.G, RGBA.B, RGBA.Alpha, we // will get separate components of Int32. [StructLayout(LayoutKind.Explicit)] public struct RGBA { [FieldOffset(0)] public uint Value; [FieldOffset(0)] public byte R; [FieldOffset(1)] public byte G; [FieldOffset(2)] public byte B; [FieldOffset(3)] public byte Alpha; } 

Você pode dizer que esse comportamento é possível apenas para tipos de valor. No entanto, você pode simulá-lo para tipos de referência, usando um endereço para sobrepor dois tipos de referência ou um tipo de referência e um tipo de valor:


 class Program { public static void Main() { Union x = new Union(); x.Reference.Value = "Hello!"; Console.WriteLine(x.Value.Value); } [StructLayout(LayoutKind.Explicit)] public class Union { public Union() { Value = new Holder<IntPtr>(); Reference = new Holder<object>(); } [FieldOffset(0)] public Holder<IntPtr> Value; [FieldOffset(0)] public Holder<object> Reference; } public class Holder<T> { public T Value; } } 

Eu usei um tipo genérico para sobrepor-se de propósito. Se eu usasse o habitual
sobreposto, esse tipo causaria a TypeLoadException quando carregada em um domínio de aplicativo. Pode parecer uma violação de segurança na teoria (especialmente quando se trata de plug-ins de aplicativos), mas se tentarmos executar esse código usando um domínio protegido, obteremos a mesma TypeLoadException .


A diferença na alocação


Outro recurso que diferencia os dois tipos é a alocação de memória para objetos ou estruturas. O CLR deve decidir sobre várias coisas antes de alocar memória para um objeto. Qual é o tamanho de um objeto? É mais ou menos que 85K? Se for menor, há espaço livre suficiente no SOH para alocar esse objeto? Se for mais, o CLR ativa o Garbage Collector. Ele percorre um gráfico de objetos, compacta os objetos, movendo-os para o espaço livre. Se ainda não houver espaço no SOH, a alocação de páginas adicionais de memória virtual será iniciada. Só então é que um objeto recebe espaço alocado, limpo do lixo. Posteriormente, o CLR estabelece SyncBlockIndex e VirtualMethodsTable. Finalmente, a referência a um objeto retorna ao usuário.


Se um objeto alocado for maior que 85K, ele será direcionado para a Pilha de Objetos Grandes (LOH). É o caso de grandes seqüências de caracteres e matrizes. Aqui, devemos encontrar o espaço mais adequado na memória da lista de intervalos desocupados ou alocar um novo. Não é rápido, mas vamos lidar com objetos desse tamanho com cuidado. Além disso, não vamos falar sobre eles aqui.


Existem vários cenários possíveis para RefTypes:


  • RefType <85K, há espaço no SOH: alocação rápida de memória;
  • RefType <85K, o espaço no SOH está acabando: alocação de memória muito lenta;
  • RefType> 85K, alocação lenta de memória.

Tais operações são raras e não podem competir com os ValTypes. O algoritmo de alocação de memória para tipos de valor não existe. A alocação de memória para tipos de valor não custa nada. A única coisa que acontece ao alocar memória para esse tipo é definir os campos como nulos. Vamos ver por que isso acontece: 1. Quando se declara uma variável no corpo de um método, o tempo de alocação de memória para uma estrutura é próximo de zero. Isso ocorre porque o tempo de alocação para variáveis ​​locais não depende do número deles; 2. Se ValTypes forem alocados como campos, Reftypes aumentará o tamanho dos campos. Um tipo de valor é alocado inteiramente, tornando-se sua parte; 3. Como no caso de cópia, se ValTypes são passados ​​como parâmetros de método, aparece uma diferença, dependendo do tamanho e do local de um parâmetro.


No entanto, isso não leva mais tempo do que copiar uma variável para outra.


A escolha entre uma classe ou uma estrutura


Vamos discutir as vantagens e desvantagens de ambos os tipos e decidir sobre seus cenários de uso. Um princípio clássico diz que devemos escolher um tipo de valor se ele não for maior que 16 bytes, permanecer inalterado durante sua vida útil e não for herdado. No entanto, escolher o tipo certo significa revisá-lo de diferentes perspectivas, com base em cenários de uso futuro. Proponho três grupos de critérios:


  • com base na arquitetura do sistema de tipos, na qual seu tipo irá interagir;
  • com base em sua abordagem como programador de sistema para escolher um tipo com desempenho ideal;
  • quando não há outra escolha.

Cada recurso projetado deve refletir seu objetivo. Isso não trata apenas do nome ou da interface de interação (métodos, propriedades). Pode-se usar considerações arquitetônicas para escolher entre os tipos de valor e referência. Vamos pensar por que uma estrutura e não uma classe podem ser escolhidas do ponto de vista do sistema de tipos.


  1. Se o seu tipo projetado for independente do estado, isso significa que o estado reflete um processo ou é um valor de alguma coisa. Em outras palavras, uma instância de um tipo é constante e imutável por natureza. Podemos criar outra instância de um tipo com base nessa constante, indicando algum deslocamento. Ou, podemos criar uma nova instância indicando suas propriedades. No entanto, não devemos mudar isso. Não quero dizer que a estrutura seja do tipo imutável. Você pode alterar seus valores de campo. Além disso, você pode passar uma referência a uma estrutura para um método usando o parâmetro ref e obterá campos alterados após sair do método. O que falo aqui é sobre o sentido arquitetônico. Vou dar vários exemplos.


    • DateTime é uma estrutura que encapsula o conceito de um momento no tempo. Ele armazena esses dados como um uint, mas fornece acesso a características separadas de um momento no tempo: ano, mês, dia, hora, minutos, segundos, milissegundos e até tiques de processador. No entanto, é imutável, com base no que encapsula. Não podemos mudar um momento no tempo. Não posso viver o próximo minuto como se fosse meu melhor aniversário de infância. Portanto, se escolhermos um tipo de dados, podemos escolher uma classe com uma interface somente leitura, que produz uma nova instância para cada alteração de propriedades. Ou, podemos escolher uma estrutura, que pode, mas não deve, alterar os campos de suas instâncias: seu valor é a descrição de um momento no tempo, como um número. Você não pode acessar a estrutura de um número e alterá-lo. Se você deseja obter outro momento no tempo, que difere por um dia do original, você receberá uma nova instância de uma estrutura.
    • KeyValuePair<TKey, TValue> é uma estrutura que encapsula o conceito de um par de valores-chave conectado. Essa estrutura é apenas para gerar o conteúdo de um dicionário durante a enumeração. Do ponto de vista arquitetônico, uma chave e um valor são conceitos inseparáveis ​​no Dictionary<T> . No entanto, por dentro, temos uma estrutura complexa, onde uma chave está separada de um valor. Para um usuário, um par de valores-chave é um conceito inseparável em termos de interface e o significado de uma estrutura de dados. É um valor inteiro em si. Se alguém atribuir outro valor para uma chave, todo o par será alterado. Assim, eles representam uma única entidade. Isso torna uma estrutura uma variante ideal nesse caso.

  2. Se o seu tipo projetado é uma parte inseparável de um tipo externo, mas é integralmente estrutural. Isso significa que é incorreto dizer que o tipo externo se refere a uma instância de um tipo encapsulado. No entanto, é correto dizer que um tipo encapsulado faz parte de um externo junto com todas as suas propriedades. Isso é útil ao projetar uma estrutura que faz parte de outra estrutura.


    • Por exemplo, se usarmos a estrutura de um cabeçalho de arquivo, não será apropriado passar uma referência de um arquivo para outro, por exemplo, algum arquivo header.txt. Isso seria apropriado ao inserir um documento em outro, não incorporando um arquivo, mas usando uma referência em um sistema de arquivos. Um bom exemplo são os arquivos de atalho no sistema operacional Windows. No entanto, se falarmos sobre um cabeçalho de arquivo (por exemplo, cabeçalho de arquivo JPEG contendo metadados sobre tamanho de imagem, métodos de compressão, parâmetros de fotografia, coordenadas GPS e outros), devemos usar estruturas para projetar tipos para analisar o cabeçalho. Se você descrever todos os cabeçalhos das estruturas, obterá a mesma posição dos campos na memória que em um arquivo. Utilizando uma transformação *(Header *)readedBuffer simples e não segura *(Header *)readedBuffer sem desserialização, você obterá estruturas de dados totalmente preenchidas.


  1. Nenhum dos exemplos mostra a herança do comportamento. Eles mostram que não há necessidade de herdar o comportamento dessas entidades. Eles são independentes. No entanto, se considerarmos a eficácia do código, veremos a escolha de outro lado:
  2. Se precisarmos pegar alguns dados estruturados de código não gerenciado, devemos escolher estruturas. Também podemos passar a estrutura de dados para um método não seguro. Um tipo de referência não é adequado para isso.
  3. Uma estrutura é sua escolha se um tipo passa os dados em chamadas de método (como valores retornados ou como parâmetro de método) e não há necessidade de se referir ao mesmo valor de locais diferentes. O exemplo perfeito é tuplas. Se um método retornar vários valores usando tuplas, ele retornará um ValueTuple, declarado como uma estrutura. O método não alocará espaço no heap, mas utilizará a pilha do encadeamento, onde a alocação de memória não custa nada.
  4. Se você criar um sistema que crie grande tráfego de instâncias com tamanho e vida útil pequenos, o uso de tipos de referência levará a um conjunto de objetos ou, se sem o conjunto de objetos, a um acúmulo de lixo não controlado no heap. Alguns objetos se transformarão em gerações mais antigas, aumentando a carga no GC. O uso de tipos de valor nesses locais (se possível) aumentará o desempenho porque nada passará para o SOH. Isso diminuirá a carga no GC e o algoritmo funcionará mais rapidamente;

Baseando-se no que eu disse, aqui estão alguns conselhos sobre o uso de estruturas:


  1. Ao escolher coleções, você deve evitar grandes matrizes que armazenam grandes estruturas. Isso inclui estruturas de dados baseadas em matrizes. Isso pode levar a uma transição para o Heap de objetos grandes e sua fragmentação. É errado pensar que, se nossa estrutura tiver 4 campos do tipo byte, serão necessários 4 bytes. Devemos entender que, nos sistemas de 32 bits, cada campo de estrutura é alinhado nos limites de 4 bytes (cada campo de endereço deve ser dividido exatamente por 4) e nos sistemas de 64 bits - nos limites de 8 bytes. O tamanho de uma matriz deve depender do tamanho de uma estrutura e plataforma, executando um programa. No nosso exemplo, com 4 bytes - 85K / (de 4 a 8 bytes por campo * o número de campos = 4) menos o tamanho do cabeçalho de uma matriz é igual a cerca de 2 600 elementos por matriz, dependendo da plataforma (isso deve ser arredondado para baixo ) Isso não é muito. Pode parecer que podemos facilmente alcançar uma constante mágica de 20.000 elementos
  2. Às vezes, você usa uma estrutura de tamanho grande como fonte de dados e a coloca como campo em uma classe, enquanto uma cópia é replicada para produzir milhares de instâncias. Então você expande cada instância de uma classe para o tamanho de uma estrutura. Isso levará ao aumento da geração zero e à transição para a geração um e até dois. Se as instâncias de uma classe tiverem um curto período de vida e você achar que o GC as coletará na geração zero - por 1 ms, você ficará desapontado. Eles já estão na geração um e até dois. Isso faz a diferença. Se o GC coletar a geração zero por 1 ms, as gerações uma e duas serão coletadas muito lentamente, o que levará a uma diminuição na eficiência;
  3. Pelo mesmo motivo, você deve evitar passar grandes estruturas por meio de uma série de chamadas de método. Se todos os elementos se chamarem, essas chamadas ocuparão mais espaço na pilha e matarão seu aplicativo por StackOverflowException. O próximo motivo é o desempenho. Quanto mais cópias houver, mais lentamente tudo funcionará.

É por isso que a escolha de um tipo de dados não é um processo óbvio. Geralmente, isso pode se referir a uma otimização prematura, o que não é recomendado. No entanto, se você souber que sua situação se enquadra nos princípios mencionados acima, poderá escolher facilmente um tipo de valor.


Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.

Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório github / sidristij / dotnetbook .

Source: https://habr.com/ru/post/pt439486/


All Articles