Estruturas versus classes

93

Estou prestes a criar 100.000 objetos em código. São pequenos, com apenas 2 ou 3 propriedades. Vou colocá-los em uma lista genérica e, quando estiverem, vou fazer um loop e verificar o valor ae talvez atualizar o valor b.

É mais rápido / melhor criar esses objetos como classe ou estrutura?

EDITAR

uma. As propriedades são tipos de valor (exceto a string, eu acho?)

b. Eles podem (não temos certeza ainda) ter um método de validação

EDITAR 2

Eu estava me perguntando: os objetos no heap e na pilha são processados ​​igualmente pelo coletor de lixo ou isso funciona de forma diferente?

Michel
fonte
2
Eles terão apenas campos públicos ou também métodos? Os tipos são tipos primitivos, como inteiros? Eles estarão contidos em uma matriz ou em algo como List <T>?
JeffFerguson de
14
Uma lista de estruturas mutáveis? Cuidado com o velociraptor.
Anthony Pegram de
1
@Anthony: receio estar perdendo a piada do velociraptor: -s
Michel
5
A piada do velociraptor é do XKCD. Mas quando você está jogando em torno de 'tipos de valor são alocados na pilha' detalhes de equívoco / implementação (exclua conforme aplicável), então é Eric Lippert que você precisa tomar cuidado ...
Greg Beech
4
velociraptor: imgs.xkcd.com/comics/goto.png
WernerCD de

Respostas:

137

É mais rápido criar esses objetos como classe ou estrutura?

Você é a única pessoa que pode determinar a resposta a essa pergunta. Tente das duas maneiras, meça uma métrica de desempenho significativa, focada no usuário e relevante, e então você saberá se a mudança tem um efeito significativo em usuários reais em cenários relevantes.

As estruturas consomem menos memória heap (porque são menores e mais facilmente compactadas, não porque estão "na pilha"). Mas eles demoram mais para copiar do que uma cópia de referência. Não sei quais são suas métricas de desempenho para uso de memória ou velocidade; há uma troca aqui e você é a pessoa que sabe o que é.

É melhor criar esses objetos como classe ou estrutura?

Talvez classe, talvez estrutura. Como regra geral: Se o objeto for:
1. Pequeno
2. Logicamente um valor imutável
3. Há muitos deles,
então eu consideraria torná-lo uma estrutura. Caso contrário, ficaria com um tipo de referência.

Se você precisar alterar algum campo de uma estrutura, geralmente é melhor construir um construtor que retorne uma nova estrutura inteira com o campo definido corretamente. Isso talvez seja um pouco mais lento (meça!), Mas logicamente muito mais fácil de raciocinar.

Os objetos no heap e na pilha são processados ​​igualmente pelo coletor de lixo?

Não , eles não são iguais porque os objetos na pilha são as raízes da coleção . O coletor de lixo não precisa se perguntar "essa coisa na pilha está viva?" porque a resposta a essa pergunta é sempre "Sim, está na pilha". (Agora, você não pode contar com isso para manter um objeto vivo porque a pilha é um detalhe de implementação. O jitter tem permissão para introduzir otimizações que, digamos, registram o que normalmente seria um valor de pilha, e então nunca está na pilha portanto, o GC não sabe que ainda está vivo. Um objeto registrado pode ter seus descendentes coletados agressivamente, assim que o registro que o mantém não será lido novamente.)

Mas o coletor de lixo não tem que tratar objetos na pilha como vivo, da mesma forma que trata qualquer objeto conhecido por estar vivo como vivo. O objeto na pilha pode se referir a objetos alocados no heap que precisam ser mantidos ativos, portanto, o GC deve tratar os objetos da pilha como objetos alocados no heap vivos para fins de determinar o conjunto ativo. Mas, obviamente, eles não são tratados como "objetos vivos" com o propósito de compactar o heap, porque eles não estão no heap em primeiro lugar.

Está claro?

Eric Lippert
fonte
Eric, você sabe se o compilador ou o jitter faz uso da imutabilidade (talvez se aplicada com readonly) para permitir otimizações. Eu não deixaria que isso afetasse uma escolha sobre mutabilidade (eu sou louco por detalhes de eficiência na teoria, mas na prática meu primeiro movimento em direção à eficiência é sempre tentar ter uma garantia de correção tão simples quanto possível e, portanto, não preciso desperdiçar ciclos de CPU e ciclos cerebrais em verificações e casos extremos, e ser apropriadamente mutável ou imutável ajuda nisso), mas seria contra qualquer reação instintiva ao fato de você dizer que a imutabilidade pode ser mais lenta.
Jon Hanna de
@Jon: O compilador C # otimiza dados const, mas não dados somente leitura . Não sei se o compilador jit executa alguma otimização de cache em campos somente leitura.
Eric Lippert,
Uma pena, como eu sei que o conhecimento da imutabilidade permite algumas otimizações, mas atingiu os limites do meu conhecimento teórico naquele ponto, mas são limites que eu adoraria estender. Nesse ínterim, "pode ​​ser mais rápido em ambos os sentidos, eis o motivo, agora teste e descubra o que se aplica neste caso" é útil poder dizer :)
Jon Hanna
Eu recomendaria ler simple-talk.com/dotnet/.net-framework/… e seu próprio artigo (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/… para começar a mergulhar em detalhes. Existem muitos outros artigos bons por aí. BTW, a diferença no processamento de 100.000 pequenos objetos na memória dificilmente é perceptível por meio de alguma sobrecarga de memória (~ 2,3 MB) para a aula. Pode ser facilmente verificado por um teste simples.
Nick Martyshchenko,
Sim, isso está claro. Muito obrigado pela sua resposta abrangente (extensa é melhor? O Google tradutor deu 2 traduções. Eu quis dizer que você não teve tempo para escrever uma resposta curta, mas também escreveu todos os detalhes).
Michel de
23

Às vezes, structvocê não precisa chamar o construtor new () e atribuir diretamente os campos, tornando-o muito mais rápido do que o normal.

Exemplo:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

é cerca de 2 a 3 vezes mais rápido do que

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

onde Valueé um structcom dois campos ( ide isValid).

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

Por outro lado, se os itens precisam ser movidos ou os tipos de valor selecionados, toda essa cópia vai atrasar você. Para obter a resposta exata, suspeito que você precise analisar seu código e testá-lo.

John alexiou
fonte
Obviamente, as coisas ficam muito mais rápidas quando você empacota valores além dos limites nativos também.
leppie de
Sugiro usar um nome diferente de list, visto que o código indicado não funcionará com a List<Value>.
supercat
7

As estruturas podem parecer semelhantes às classes, mas existem diferenças importantes das quais você deve estar ciente. Em primeiro lugar, classes são tipos de referência e structs são tipos de valor. Usando structs, você pode criar objetos que se comportam como os tipos integrados e também aproveitar seus benefícios.

Quando você chama o operador New em uma classe, ele é alocado no heap. No entanto, quando você instancia uma estrutura, ela é criada na pilha. Isso produzirá ganhos de desempenho. Além disso, você não lidará com referências a uma instância de uma estrutura como faria com as classes. Você trabalhará diretamente com a instância de struct. Por isso, ao passar uma estrutura para um método, ela é passada por valor em vez de como uma referência.

Mais aqui:

http://msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx

Kyndigs
fonte
4
Eu sei que diz isso no MSDN, mas o MSDN não conta toda a história. Pilha versus pilha é um detalhe de implementação e as estruturas nem sempre vão para a pilha. Para ver apenas um blog recente sobre isso, consulte: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram
"... é passado por valor ..." ambas as referências e estruturas são passadas por valor (a menos que se use 'ref') - é se um valor ou referência está sendo passado que difere, ou seja, structs são passados ​​valor a valor , os objetos de classe são passados ​​como referência por valor e os parâmetros marcados com ref passam referência por referência.
Paul Ruane,
10
Esse artigo é enganoso em vários pontos-chave, e pedi à equipe do MSDN para revisá-lo ou excluí-lo.
Eric Lippert,
2
@supercat: para abordar seu primeiro ponto: o ponto mais importante é que no código gerenciado, onde um valor ou referência a um valor é armazenado, é amplamente irrelevante . Trabalhamos muito para criar um modelo de memória que na maioria das vezes permitisse aos desenvolvedores permitir que o tempo de execução tomasse decisões inteligentes de armazenamento em seu nome. Essas distinções são muito importantes quando a falha em entendê-las tem consequências desastrosas, como acontece em C; não tanto em C #.
Eric Lippert,
1
@supercat: para abordar seu segundo ponto, nenhuma estrutura mutável é principalmente maligna. Por exemplo, void M () {S s = new S (); s.Blah (); N (s); } Refatorar para: void DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Isso acaba de introduzir um bug porque S é uma estrutura mutável. Você viu imediatamente o bug? Ou o fato de S é uma estrutura mutável esconde o bug de você?
Eric Lippert,
6

Arrays de structs são representados no heap em um bloco contíguo de memória, enquanto um array de objetos é representado como um bloco contíguo de referências com os próprios objetos reais em outro lugar no heap, exigindo memória tanto para os objetos quanto para suas referências de array .

Neste caso, como você os está colocando em a List<>(e a List<>está apoiado em um array), seria mais eficiente, em termos de memória, usar structs.

(Porém, tenha cuidado, pois grandes arrays encontrarão seu caminho no Large Object Heap onde, se sua vida útil for longa, pode ter um efeito adverso no gerenciamento de memória do seu processo. Lembre-se, também, que a memória não é a única consideração.)

Paul Ruane
fonte
Você pode usar refpalavras-chave para lidar com isso.
leppie de
"Cuidado, porém, que grandes arrays encontrarão seu caminho no Large Object Heap onde, se sua vida útil for longa, pode ter um efeito adverso no gerenciamento de memória do seu processo." - Não sei bem por que você acha isso? Ser alocado no LOH não causará nenhum efeito adverso no gerenciamento de memória, a menos (possivelmente) que seja um objeto de curta duração e você queira recuperar a memória rapidamente sem esperar por uma coleta de Gen 2.
Jon Artus
@Jon Artus: o LOH não é compactado. Qualquer objeto de longa duração dividirá o LOH na área de memória livre anterior e na área posterior. A memória contígua é necessária para a alocação e se essas áreas não forem grandes o suficiente para uma alocação, então mais memória é alocada para o LOH (ou seja, você obterá fragmentação do LOH).
Paul Ruane
4

Se eles tiverem semântica de valor, você provavelmente deve usar uma estrutura. Se eles tiverem semântica de referência, você provavelmente deve usar uma classe. Existem exceções, que geralmente tendem a criar uma classe mesmo quando há semântica de valor, mas comece a partir daí.

Quanto à sua segunda edição, o GC lida apenas com o heap, mas há muito mais espaço do heap do que espaço da pilha, portanto, colocar coisas na pilha nem sempre é uma vitória. Além disso, uma lista de tipos de estrutura e uma lista de tipos de classe estarão no heap de qualquer maneira, portanto, isso é irrelevante neste caso.

Editar:

Estou começando a considerar o termo mal prejudicial. Afinal, tornar uma classe mutável é uma má ideia se não for ativamente necessária, e eu não descartaria o uso de uma estrutura mutável. No entanto, é uma ideia ruim com tanta frequência que quase sempre é uma má ideia, mas principalmente ela simplesmente não coincide com a semântica de valor, então simplesmente não faz sentido usar uma estrutura no caso dado.

Pode haver exceções razoáveis ​​com estruturas aninhadas privadas, onde todos os usos dessa estrutura são, portanto, restritos a um escopo muito limitado. Porém, isso não se aplica aqui.

Realmente, eu acho que "muda, então é um mau stuct" não é muito melhor do que continuar sobre o heap e a pilha (o que pelo menos tem algum impacto no desempenho, mesmo que frequentemente deturpado). "Ele sofre mutação, então provavelmente não faz sentido considerá-lo como tendo semântica de valor, então é uma estrutura ruim" é apenas um pouco diferente, mas é importante, eu acho.

Jon Hanna
fonte
3

A melhor solução é medir, medir novamente e então medir um pouco mais. Pode haver detalhes do que você está fazendo que podem dificultar uma resposta simplificada e fácil, como "usar estruturas" ou "usar classes".

FMM
fonte
concordo com a parte da medida, mas na minha opinião foi um exemplo direto e claro, e pensei que talvez algumas coisas genéricas pudessem ser ditas sobre isso. E, como se viu, algumas pessoas o fizeram.
Michel de
3

Uma estrutura é, em seu cerne, nada mais nada menos do que uma agregação de campos. No .NET é possível que uma estrutura "finja" ser um objeto, e para cada tipo de estrutura o .NET define implicitamente um tipo de objeto heap com os mesmos campos e métodos que - sendo um objeto heap - se comportarão como um objeto . Uma variável que contém uma referência a tal objeto heap (estrutura "em caixa") exibirá semântica de referência, mas aquela que contém uma estrutura diretamente é simplesmente uma agregação de variáveis.

Acho que grande parte da confusão estrutura versus classe vem do fato de que as estruturas têm dois casos de uso muito diferentes, que devem ter diretrizes de design muito diferentes, mas as diretrizes da MS não fazem distinção entre eles. Às vezes, há necessidade de algo que se comporte como um objeto; nesse caso, as diretrizes da MS são bastante razoáveis, embora o "limite de 16 bytes" provavelmente deva ser mais parecido com 24-32. Às vezes, no entanto, o que é necessário é uma agregação de variáveis. Uma estrutura usada para essa finalidade deve consistir simplesmente em um monte de campos públicos e, possivelmente, uma Equalssubstituição, uma ToStringsubstituição eIEquatable(itsType).Equalsimplementação. Estruturas que são usadas como agregações de campos não são objetos e não deveriam fingir ser. Do ponto de vista da estrutura, o significado de campo deve ser nada mais nada menos que "a última coisa escrita neste campo". Qualquer significado adicional deve ser determinado pelo código do cliente.

Por exemplo, se uma estrutura de agregação de variável tem membros Minimume Maximum, a própria estrutura não deve prometer isso Minimum <= Maximum. Código que recebe a estrutura tal como um parâmetro deve se comportar como se fosse aprovada separada Minimume Maximumvalores. Um requisito que Minimumnão seja maior do que Maximumdeve ser considerado como um requisito de que um Minimumparâmetro não seja maior do que um aprovado separadamente Maximum.

Um padrão útil a ser considerado às vezes é ter uma ExposedHolder<T>classe definida como:

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

Se alguém tem um List<ExposedHolder<someStruct>>, onde someStructé uma estrutura de agregação de variável, pode-se fazer coisas como myList[3].Value.someField += 7;, mas fornecer myList[3].Valuea outro código fornecerá o conteúdo de, em Valuevez de fornecer um meio de alterá-lo. Por outro lado, se um usasse um List<someStruct>, seria necessário usar var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Se alguém usasse um tipo de classe mutável, expor o conteúdo do myList[3]código externo exigiria a cópia de todos os campos para algum outro objeto. Se alguém usasse um tipo de classe imutável ou uma estrutura de "estilo de objeto", seria necessário construir uma nova instância semelhante, myList[3]exceto pela someFieldqual fosse diferente, e então armazenar essa nova instância na lista.

Uma observação adicional: se você estiver armazenando um grande número de coisas semelhantes, pode ser bom armazená-las em matrizes de estruturas possivelmente aninhadas, de preferência tentando manter o tamanho de cada matriz entre 1K e 64K ou algo assim. Os arranjos de estruturas são especiais, na medida em que uma indexação produzirá uma referência direta a uma estrutura interna, então pode-se dizer "a [12] .x = 5;". Embora seja possível definir objetos semelhantes a matrizes, C # não permite que eles compartilhem essa sintaxe com matrizes.

supergato
fonte
1

Use aulas.

Em uma nota geral. Por que não atualizar o valor b conforme você os cria?

Preet Sangha
fonte
1

De uma perspectiva do c ++, concordo que será mais lento modificar as propriedades de uma estrutura em comparação com uma classe. Mas eu realmente acho que eles serão mais rápidos de ler devido à estrutura sendo alocada na pilha em vez de no heap. Ler dados do heap requer mais verificações do que da pilha.

Robert
fonte
1

Bem, se você usar struct afinal, livre-se da string e use char ou buffer de bytes de tamanho fixo.

Isso é re: desempenho.

Daniel Mošmondor
fonte