Quebrando os fundamentos fundamentais do C #: alocando memória para um tipo de referência na pilha

Neste artigo, serão fornecidos os conceitos básicos do dispositivo de tipo interno, bem como um exemplo no qual a memória para o tipo de referência será alocada completamente na pilha (isso porque eu sou um programador de pilha completa).



Isenção de responsabilidade


Este artigo não contém material que deve ser usado em projetos reais. É simplesmente uma extensão dos limites em que uma linguagem de programação é percebida.

Antes de começar a história, recomendo fortemente que você leia o primeiro post sobre o StructLayout , porque lá é analisado um exemplo que será usado neste artigo (no entanto, como sempre).

Antecedentes


Começando a escrever o código deste artigo, eu queria fazer algo interessante usando a linguagem assembly. Eu queria, de alguma forma, quebrar o modelo de execução padrão e obter um resultado realmente incomum. E lembrando com que frequência as pessoas dizem que o tipo de referência difere do significativo, pois o primeiro está localizado na pilha e o segundo na pilha, decidi usar o assembler para mostrar que o tipo de referência pode viver na pilha. No entanto, comecei a encontrar todos os tipos de problemas, por exemplo, retornando o endereço desejado e representando-o como um link gerenciado (ainda estou trabalhando nele). Então comecei a enganar e fazer o que não funciona no assembler, em C #. E, no final, montador não permaneceu.
Além disso, uma recomendação para leitura - se você estiver familiarizado com o dispositivo dos tipos de referência, recomendo que você pule a teoria sobre eles (apenas o básico será fornecido, nada de interessante).

Um pouco sobre a estrutura interna dos tipos


Gostaria de lembrá-lo de que a separação da memória na pilha e na pilha ocorre no nível .NET, e essa divisão é puramente lógica, fisicamente não há diferença entre as áreas de memória na pilha e na pilha. A diferença de produtividade já é fornecida especificamente ao trabalhar com essas áreas.

Como então alocar memória na pilha? Para começar, vamos ver como esse tipo de referência misterioso está estruturado e o que está nele, o que não é significativo.

Portanto, considere o exemplo mais simples com a classe Employee.

Código do funcionário
public class Employee { private int _id; private string _name; public virtual void Work() { Console.WriteLine(“Zzzz...”); } public void TakeVacation(int days) { Console.WriteLine(“Zzzz...”); } public static void SetCompanyPolicy(CompanyPolicy policy) { Console.WriteLine("Zzzz..."); } } 


E veja como é apresentado na memória.
UPD: Esta classe é considerada no exemplo de um sistema de 32 bits.



Assim, além da memória para os campos, temos mais dois campos ocultos - o índice do bloco de sincronização (a palavra do título do objeto na figura) e o endereço da tabela de métodos.

O primeiro campo, é o índice do bloco de sincronização, não estamos particularmente interessados. Ao colocar o tipo, decidi omiti-lo. Eu fiz isso por dois motivos:

  1. Sou muito preguiçoso (não disse que os motivos serão razoáveis)
  2. Este campo é opcional para o funcionamento básico do objeto.

Mas como já falamos, acho correto dizer algumas palavras sobre esse campo. É usado para diferentes finalidades (código hash, sincronização). Em vez disso, o próprio campo é simplesmente o índice de um dos blocos de sincronização associados a esse objeto. Os próprios blocos estão localizados na tabela de blocos de sincronização (à la global array). Criar um bloco como esse é uma operação bastante grande, portanto, ele não será criado se não for necessário. Além disso, ao usar bloqueios finos, o identificador do segmento que recebeu o bloqueio (em vez do índice) será gravado lá.

O segundo campo é muito mais importante para nós. Graças à tabela de métodos de tipos, é possível uma ferramenta tão poderosa como o polimorfismo (que, aliás, não é possuído pela estrutura, pelos reis da pilha). Suponha que a classe Employee adicionalmente implemente três interfaces: IComparable, IDisposable e ICloneable.

Em seguida, a tabela de métodos será mais ou menos assim



A imagem é muito legal, lá, em princípio, tudo é pintado e compreensível. Se houver poucos dedos, o método virtual será chamado não diretamente no endereço, mas pelo deslocamento na tabela de métodos. Na hierarquia, os mesmos métodos virtuais estarão localizados no mesmo deslocamento na tabela de métodos. Ou seja, chamamos o método na classe base no deslocamento, sem saber qual tipo de tabela de método será usado, mas sabendo que nesse deslocamento haverá o método mais relevante para o tipo de tempo de execução.

Também vale lembrar que a referência ao objeto aponta para a tabela de métodos.

O tão esperado exemplo


Vamos começar com aulas que nos ajudarão em nosso objetivo. Usando o StructLayout (eu realmente tentei sem ele, mas não deu certo), escrevi os mapeadores de ponteiros mais simples para tipos gerenciados e vice-versa. É muito fácil obter um ponteiro de um link gerenciado, mas a transformação inversa me causou dificuldades e, sem pensar duas vezes, apliquei meu atributo favorito. Para manter o código em uma chave, eu o fiz em duas direções de uma maneira.

Código aqui
 //     public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } //     public class PointerCasterUnderground { public virtual T GetManagedReferenceByPointer<T>(T reference) => reference; public virtual unsafe int* GetPointerByManagedReference<T>(int* pointer) => pointer; } [StructLayout(LayoutKind.Explicit)] public class PointerCaster { public PointerCaster() { pointerCaster= new PointerCasterUnderground(); } [FieldOffset(0)] private PointerCasterUnderground pointerCaster; [FieldOffset(0)] public PointerCasterFacade Caster; } 


Primeiro, escreva um método que leve um ponteiro para alguma memória (não necessariamente na pilha, a propósito) e configure o tipo.

Para facilitar a localização do endereço da tabela de métodos, crio um tipo no heap. Estou certo de que a tabela de métodos pode ser encontrada de outras maneiras, mas não me propus a otimizar esse código; era mais interessante para mim torná-lo compreensível. Em seguida, usando os conversores descritos anteriormente, obtemos um ponteiro para o tipo criado.

Este ponteiro aponta exatamente para a tabela de métodos. Portanto, basta obter o conteúdo da memória para a qual aponta. Este será o endereço da tabela de métodos.
E como o ponteiro passado para nós é uma espécie de referência ao objeto, devemos anotar o endereço da tabela de métodos exatamente onde ele aponta.

Isso é tudo, na verdade. Inesperadamente, certo? Agora nosso tipo está pronto. Pinóquio, que nos alocou a memória, cuidará da inicialização dos campos.

Resta apenas usar o avô para converter o ponteiro em um link gerenciado.

 public class StackInitializer { public static unsafe T InitializeOnStack<T>(int* pointer) where T : new() { T r = new T(); var caster = new PointerCaster().Caster; int* ptr = caster.GetPointerByManagedReference(r); pointer[0] = ptr[0]; T reference = caster.GetManagedReferenceByPointer<T>(pointer); return reference; } } 

Agora, temos um link na pilha que aponta para a mesma pilha, onde, por todas as leis dos tipos de referência (bem, quase), reside um objeto construído a partir de terra preta e paus. Polimorfismo está disponível.

Deve-se entender que, se você passar esse link fora do método, depois de retornar dele, obteremos algo pouco claro. Não se pode falar de chamadas para métodos virtuais; vamos voar por exceção. Os métodos regulares são chamados diretamente, no código simplesmente haverá endereços para métodos reais, para que funcionem. E no lugar dos campos estará ... mas ninguém sabe o que estará lá.

Como é impossível usar um método separado para inicialização na pilha (já que o quadro da pilha será apagado após o retorno do método), a memória deve ser alocada pelo método que deseja usar o tipo na pilha. Estritamente falando, não há uma maneira de fazer isso. Mas o mais adequado para nós é o stackalloc. Apenas a palavra-chave perfeita para nossos propósitos. Infelizmente, foi isso que introduziu incontrolabilidade no código. Antes disso, havia uma idéia de usar o Span para esses fins e sem código inseguro. Não há nada errado com o código não seguro, mas, como em qualquer outro lugar, não é uma bala de prata e possui suas próprias áreas de aplicação.

Em seguida, depois de receber um ponteiro para a memória na pilha atual, passamos esse ponteiro para um método que compõe o tipo em partes. Foi tudo o que ouviu - muito bem.

 unsafe class Program { public static void Main() { int* pointer = stackalloc int[2]; var a = StackInitializer.InitializeOnStack<StackReferenceType>(pointer); a.StubMethod(); Console.WriteLine(a.Field); Console.WriteLine(a); Console.Read(); } } 

Você não deve usar isso em projetos reais, o método que aloca memória na pilha usa o novo T (), que por sua vez usa reflexão para criar o tipo na pilha! Portanto, esse método será mais lento que a criação usual do tipo uma vez, bem, 40-50.

Aqui você pode ver o projeto inteiro.

Fonte: em uma digressão teórica, foram utilizados exemplos do livro Sasha Goldstein - Pro .NET Performace

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


All Articles