O uso de "novo" em uma estrutura o aloca na pilha ou pilha?

290

Quando você cria uma instância de uma classe com o newoperador, a memória é alocada no heap. Quando você cria uma instância de uma estrutura com o newoperador onde a memória é alocada, no heap ou na pilha?

kedar kamthe
fonte

Respostas:

305

Ok, vamos ver se posso deixar isso mais claro.

Em primeiro lugar, Ash está certo: a questão não é sobre onde as variáveis ​​de tipo de valor são alocadas. Essa é uma pergunta diferente - e uma para a qual a resposta não está apenas "na pilha". É mais complicado do que isso (e tornado ainda mais complicado pelo C # 2). Eu tenho um artigo sobre o tópico e o expandirei, se solicitado, mas vamos lidar apenas com o newoperador.

Em segundo lugar, tudo isso realmente depende do nível que você está falando. Estou vendo o que o compilador faz com o código-fonte, em termos de IL que ele cria. É mais do que possível que o compilador JIT faça coisas inteligentes em termos de otimizar bastante a alocação "lógica".

Em terceiro lugar, estou ignorando os genéricos, principalmente porque na verdade não sei a resposta, e em parte porque isso complicaria demais as coisas.

Finalmente, tudo isso é apenas com a implementação atual. A especificação C # não especifica muito disso - é efetivamente um detalhe de implementação. Há quem acredite que os desenvolvedores de código gerenciado realmente não deveriam se importar. Não tenho certeza se chegaria tão longe, mas vale a pena imaginar um mundo onde, de fato, todas as variáveis ​​locais residem na pilha - o que ainda estaria em conformidade com as especificações.


Existem duas situações diferentes com o newoperador nos tipos de valor: você pode chamar um construtor sem parâmetros (por exemplo new Guid()) ou um construtor com parâmetros (por exemplo new Guid(someString)). Estes geram IL significativamente diferente. Para entender o porquê, você precisa comparar as especificações C # e CLI: de acordo com C #, todos os tipos de valor têm um construtor sem parâmetros. De acordo com a especificação da CLI, nenhum tipo de valor possui construtores sem parâmetros. (Busque os construtores de um tipo de valor com reflexão algum tempo - você não encontrará um sem parâmetro.)

Faz sentido para o C # tratar a "inicializar um valor com zeros" como um construtor, porque mantém a linguagem consistente - você pode pensar new(...)em sempre chamar um construtor. Faz sentido para a CLI pensar de maneira diferente, pois não há código real a ser chamado - e certamente não há código específico ao tipo.

Também faz diferença o que você fará com o valor depois de inicializá-lo. O IL usado para

Guid localVariable = new Guid(someString);

é diferente da IL usada para:

myInstanceOrStaticVariable = new Guid(someString);

Além disso, se o valor for usado como um valor intermediário, por exemplo, um argumento para uma chamada de método, as coisas serão ligeiramente diferentes novamente. Para mostrar todas essas diferenças, aqui está um pequeno programa de teste. Não mostra a diferença entre variáveis ​​estáticas e variáveis ​​de instância: a IL seria diferente entre stflde stsfld, mas é tudo.

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

Aqui está o IL da classe, excluindo bits irrelevantes (como nops):

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

Como você pode ver, existem muitas instruções diferentes usadas para chamar o construtor:

  • newobj: Aloca o valor na pilha, chama um construtor parametrizado. Utilizado para valores intermediários, por exemplo, para atribuição a um campo ou uso como argumento de método.
  • call instance: Usa um local de armazenamento já alocado (na pilha ou não). Isso é usado no código acima para atribuir a uma variável local. Se a mesma variável local recebe um valor várias vezes usando várias newchamadas, ela apenas inicializa os dados por cima do valor antigo - ela não aloca mais espaço de pilha a cada vez.
  • initobj: Usa um local de armazenamento já alocado e apenas limpa os dados. Isso é usado para todas as nossas chamadas de construtor sem parâmetros, incluindo aquelas atribuídas a uma variável local. Para a chamada do método, uma variável local intermediária é efetivamente introduzida e seu valor é apagado por initobj.

Espero que isso mostre o quão complicado o tópico é, enquanto brilha um pouco de luz ao mesmo tempo. Em alguns aspectos conceituais, toda chamada newaloca espaço na pilha - mas, como vimos, não é o que realmente acontece, mesmo no nível de IL. Eu gostaria de destacar um caso em particular. Tome este método:

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

Isso "logicamente" tem 4 alocações de pilha - uma para a variável e uma para cada uma das três newchamadas - mas, de fato (para esse código específico), a pilha é alocada apenas uma vez e, em seguida, o mesmo local de armazenamento é reutilizado.

EDIT: Só para esclarecer, isso só é verdade em alguns casos ... em particular, o valor de guidnão será visível se o Guidconstrutor lançar uma exceção, e é por isso que o compilador C # é capaz de reutilizar o mesmo slot de pilha. Consulte a postagem do blog de Eric Lippert sobre construção de tipo de valor para obter mais detalhes e um caso em que não se aplica.

Aprendi muito ao escrever esta resposta - peça esclarecimentos se algo não estiver claro!

Jon Skeet
fonte
1
Jon, o código de exemplo HowManyStackAllocations é bom. Mas você pode alterá-lo para usar um Struct em vez de Guid ou adicionar um novo exemplo do Struct. Eu acho que isso iria resolver diretamente a questão original de @ kedar.
Ash
9
Guid já é uma estrutura. Veja msdn.microsoft.com/en-us/library/system.guid.aspx eu não teria escolhido um tipo de referência para esta pergunta :)
Jon Skeet
1
O que acontece quando você tem List<Guid>e adiciona esses 3 a ele? Seriam 3 alocações (mesma IL)? Mas eles são mantidos em algum lugar mágico
Arec Barrwin
1
@Ani: Você está perdendo o fato de que o exemplo de Eric tem um bloco try / catch - portanto, se uma exceção for lançada durante o construtor da estrutura, você precisará ver o valor antes do construtor. Meu exemplo não têm tal situação - se o construtor falha com uma exceção, não importa se o valor guidfoi apenas meia-substituídas, já que não será de qualquer maneira visível.
Jon Skeet
2
@Ani: Na verdade, Eric chama isso perto da parte inferior de seu post: "Agora, e o ponto de Wesner? Sim, de fato, se é uma variável local alocada por pilha (e não um campo em um fechamento) que é declarada no mesmo nível de "tentativa" de aninhamento da chamada do construtor, não passamos por esse rigamarole de criar um novo temporário, inicializar o temporário e copiá-lo para o local. Nesse caso específico (e comum), podemos otimizar a criação do temporário e da cópia, porque é impossível para um programa C # observar a diferença! "
Jon Skeet
40

A memória que contém os campos de uma estrutura pode ser alocada na pilha ou na pilha, dependendo das circunstâncias. Se a variável do tipo struct for uma variável local ou parâmetro que não é capturada por alguma classe de representante ou iterador anônimo, ela será alocada na pilha. Se a variável fizer parte de alguma classe, ela será alocada dentro da classe no heap.

Se a estrutura estiver alocada no heap, não é necessário chamar o novo operador para alocar a memória. O único objetivo seria definir os valores do campo de acordo com o que estiver no construtor. Se o construtor não for chamado, todos os campos obterão seus valores padrão (0 ou nulo).

Da mesma forma para estruturas alocadas na pilha, exceto que o C # exige que todas as variáveis ​​locais sejam definidas com algum valor antes de serem usadas, portanto, você deve chamar um construtor personalizado ou o construtor padrão (um construtor que não usa parâmetros está sempre disponível para estruturas).

Jeffrey L Whitledge
fonte
13

Em termos compactos, new é um nome impróprio para estruturas, chamando new simplesmente chama o construtor. O único local de armazenamento para a estrutura é o local definido.

Se for uma variável de membro, ela será armazenada diretamente no que for definido, se for uma variável ou parâmetro local, será armazenada na pilha.

Compare isso com as classes, que têm uma referência onde quer que a estrutura fosse armazenada na íntegra, enquanto a referência aponta para algum lugar na pilha. (Membro dentro, local / parâmetro na pilha)

Pode ajudar a olhar um pouco para C ++, onde não há distinção real entre classe / estrutura. (Existem nomes semelhantes no idioma, mas eles se referem apenas à acessibilidade padrão das coisas) Quando você chama de novo, recebe um ponteiro para o local da pilha, enquanto que se você tiver uma referência que não seja de ponteiro, ele é armazenado diretamente na pilha ou dentro do outro objeto, ala structs em C #.

Guvante
fonte
5

Como em todos os tipos de valor, as estruturas sempre vão para onde foram declaradas .

Veja esta pergunta aqui para obter mais detalhes sobre quando usar estruturas. E esta pergunta aqui para mais algumas informações sobre estruturas.

Edit: Eu tinha respondido obscankely que eles sempre vão na pilha. Isto está incorreto .

Esteban Araya
fonte
"as estruturas sempre vão para onde foram declaradas", isso é um pouco confuso. Um campo struct em uma classe é sempre colocado na "memória dinâmica quando uma instância do tipo é construída" - Jeff Richter. Isso pode estar indiretamente no heap, mas não é o mesmo que um tipo de referência normal.
Ash
Não, acho que está exatamente correto - mesmo que não seja o mesmo que um tipo de referência. O valor de uma variável vive onde é declarado. O valor de uma variável do tipo de referência é uma referência, em vez dos dados reais, é tudo.
Jon Skeet
Em resumo, sempre que você cria (declara) um tipo de valor em qualquer lugar do método, ele sempre é criado na pilha.
Ash
2
Jon, você sente falta do meu argumento. A razão pela qual essa pergunta foi feita pela primeira vez é que não está claro para muitos desenvolvedores (inclusive eu até ler CLR Via C #) onde uma estrutura é alocada se você usar o novo operador para criá-la. Dizer "as estruturas sempre vão aonde foram declaradas" não é uma resposta clara.
Ash
1
@ Ash: Se eu tiver tempo, tentarei escrever uma resposta quando chegar ao trabalho. É um tópico muito grande para tentar cobrir o trem :)
Jon Skeet
4

Provavelmente estou perdendo alguma coisa aqui, mas por que nos preocupamos com alocação?

Os tipos de valor são passados ​​por value;) e, portanto, não podem ser alterados em um escopo diferente daquele em que são definidos. Para poder alterar o valor, você deve adicionar a palavra-chave [ref].

Os tipos de referência são passados ​​por referência e podem ser alterados.

É claro que existem tipos de referências imutáveis, sendo as mais populares.

Layout / inicialização da matriz: Tipos de valor -> memória zero [nome, zip] [nome, zip] Tipos de referência -> memória zero -> nulo [ref] [ref]

user18579
fonte
3
Os tipos de referência não são passados ​​por referência - as referências são passadas por valor. Isso é muito diferente.
Jon Skeet
2

Uma declaração classou structé como um blueprint usado para criar instâncias ou objetos em tempo de execução. Se você definir uma classou structchamada Pessoa, Pessoa é o nome do tipo. Se você declarar e inicializar uma variável p do tipo Person, p é considerado um objeto ou instância de Person. Várias instâncias do mesmo tipo de Pessoa podem ser criadas e cada instância pode ter valores diferentes em propertiese fields.

A classé um tipo de referência. Quando um objeto do classé criado, a variável à qual o objeto está atribuído mantém apenas uma referência a essa memória. Quando a referência do objeto é atribuída a uma nova variável, a nova variável se refere ao objeto original. As alterações feitas através de uma variável são refletidas na outra variável porque ambas se referem aos mesmos dados.

A structé um tipo de valor. Quando a structé criado, a variável à qual structé atribuído retém os dados reais da estrutura. Quando o structé atribuído a uma nova variável, ele é copiado. A nova variável e a variável original, portanto, contêm duas cópias separadas dos mesmos dados. As alterações feitas em uma cópia não afetam a outra cópia.

Em geral, classessão usados ​​para modelar comportamentos mais complexos ou dados que devem ser modificados após a classcriação de um objeto. Structssão mais adequados para pequenas estruturas de dados que contêm principalmente dados que não devem ser modificados após a structcriação.

para mais...

Sujit
fonte
1

Praticamente as estruturas consideradas tipos de valor são alocadas na pilha, enquanto os objetos são alocados na pilha, enquanto a referência de objeto (ponteiro) é alocada na pilha.

bashmohandes
fonte
1

As estruturas são alocadas para a pilha. Aqui está uma explicação útil:

Estruturas

Além disso, as classes quando instanciadas no .NET alocam memória no heap ou no espaço de memória reservado do .NET. Considerando que as estruturas produzem mais eficiência quando instanciadas devido à alocação na pilha. Além disso, deve-se notar que a passagem de parâmetros dentro de estruturas é feita por valor.

DaveK
fonte
5
Isso não cobre o caso quando uma estrutura faz parte de uma classe - nesse ponto, ela fica na pilha, com o restante dos dados do objeto.
Jon Skeet
1
Sim, mas ele realmente se concentra e responde à pergunta que está sendo feita. Votado.
Ash
... enquanto ainda está incorreto e enganoso. Desculpe, mas não há respostas curtas para esta pergunta - a Jeffrey's é a única resposta completa.
Marc Gravell