A recomendação da Microsoft para usar propriedades C # é aplicável ao desenvolvimento de jogos?

26

Eu entendo que às vezes você precisa de propriedades, como:

public int[] Transitions { get; set; }

ou:

[SerializeField] private int[] m_Transitions;
public int[] Transitions { get { return m_Transitions; } set { m_Transitions = value; } }

Mas minha impressão é que, no desenvolvimento de jogos, a menos que você tenha uma razão para fazer o contrário, tudo deve ser o mais rápido possível. Minha impressão das coisas que li é que o Unity 3D não tem certeza de acessar o acesso via propriedades, portanto, usar uma propriedade resultará em uma chamada de função extra.

Posso ignorar constantemente as sugestões do Visual Studio para não usar campos públicos? (Observe que usamos int Foo { get; private set; }onde há uma vantagem em mostrar o uso pretendido.)

Estou ciente de que a otimização prematura é ruim, mas como eu disse, no desenvolvimento de jogos você não faz algo lento quando um esforço semelhante pode torná-lo rápido.

piojo
fonte
34
Sempre verifique com um criador de perfil antes de acreditar que algo está "lento". Foi-me dito que algo estava "lento" e o criador de perfil me disse que era necessário executar 500 milhões de vezes para perder meio segundo. Como nunca seria executado mais do que algumas centenas de vezes em um quadro, decidi que era irrelevante.
Almo
5
Descobri que um pouco de abstração aqui e ali ajuda a aplicar otimizações de baixo nível mais amplamente. Por exemplo, eu já vi vários projetos C simples usando vários tipos de listas vinculadas, que são terrivelmente lentas em comparação com matrizes contíguas (por exemplo, o suporte para ArrayList). Isso ocorre porque C não possui genéricos e esses programadores de C provavelmente acharam mais fácil reimplementar repetidamente listas vinculadas do que fazer o mesmo no ArrayListsalgoritmo de redimensionamento. Se você tiver uma boa otimização de baixo nível, encapsule-a!
hegel5000
3
@Almo Você aplica a mesma lógica a operações que ocorrem em 10.000 locais diferentes? Porque esse é o acesso de membro da classe. Logicamente, você deve multiplicar o custo de desempenho por 10.000 antes de decidir que a classe de otimização não importa. E 0,5 ms é uma regra de ouro melhor para quando o desempenho do seu jogo será arruinado, e não meio segundo. Seu argumento ainda permanece, embora eu não ache que a ação certa seja tão clara quanto você imagina.
piojo 16/08
5
Você tem 2 cavalos. Corra com eles.
Mast
@piojo eu não. Algum pensamento deve sempre entrar nessas coisas. No meu caso, era para loops no código da interface do usuário. Uma instância da interface do usuário, alguns loops, poucas iterações. Para o registro, votei no seu comentário. :)
Almo 16/08

Respostas:

44

Mas minha impressão é que, no desenvolvimento de jogos, a menos que você tenha uma razão para fazer o contrário, tudo deve ser o mais rápido possível.

Não necessariamente. Assim como no software aplicativo, há um código em um jogo que é crítico para o desempenho e código que não é.

Se o código for executado milhares de vezes por quadro, essa otimização de baixo nível poderá fazer sentido. (Embora você possa primeiro verificar primeiro se é realmente necessário chamá-lo com frequência. O código mais rápido é o código que você não executa)

Se o código for executado a cada dois segundos, a legibilidade e a manutenção são muito mais importantes que o desempenho.

Eu li que o Unity 3D não tem certeza de acessar o acesso via propriedades, portanto, usar uma propriedade resultará em uma chamada de função extra.

Com propriedades triviais no estilo de int Foo { get; private set; }você, pode ter certeza de que o compilador otimizará o wrapper de propriedade. Mas:

  • se você realmente tem uma implementação, não pode mais ter certeza. Quanto mais complexa é a implementação, menor a probabilidade de o compilador poder incorporá-la.
  • As propriedades podem ocultar a complexidade. Esta é uma faca de dois gumes. Por um lado, torna seu código mais legível e mutável. Mas, por outro lado, outro desenvolvedor que acessa uma propriedade da sua classe pode pensar que está apenas acessando uma única variável e não perceber quanto código você realmente escondeu por trás dessa propriedade. Quando uma propriedade começa a se tornar computacionalmente cara, eu tendem a refatorá-la para um método GetFoo()/ explícito SetFoo(value): implicar que há muito mais acontecendo nos bastidores. (ou se eu precisar ter ainda mais certeza de que outros entendem a dica: CalculateFoo()/ SwitchFoo(value))
Philipp
fonte
O acesso a um campo dentro de um campo público não somente leitura do tipo de estrutura é rápido, mesmo que esse campo esteja dentro de uma estrutura que esteja dentro de uma estrutura etc., desde que o objeto de nível superior seja uma classe não somente leitura campo ou uma variável autônoma não somente leitura. As estruturas podem ser muito grandes sem afetar adversamente o desempenho se essas condições se aplicarem. A quebra desse padrão causará uma desaceleração proporcional ao tamanho da estrutura.
supercat
5
"então eu tendem a refatorá-lo em um método explícito GetFoo () / SetFoo (valor):" - Bom ponto, eu tendem a mover operações pesadas das propriedades para métodos.
Mark Rogers
Existe uma maneira de confirmar que o compilador Unity 3D faz a otimização mencionada (nas versões IL2CPP e Mono para Android)?
piojo 16/08
9
@piojo Como na maioria das questões de desempenho, a resposta mais simples é "apenas faça". Você tem o código pronto - teste a diferença entre ter um campo e ter uma propriedade. Vale a pena investigar os detalhes da implementação quando você pode realmente medir uma diferença importante entre as duas abordagens. Na minha experiência, se você escrever tudo para ser o mais rápido possível, você limitará as otimizações de alto nível disponíveis e apenas tentará correr pelas partes de baixo nível ("não é possível ver a floresta para as árvores"). Por exemplo, encontrar maneiras de pularUpdate completamente economizará mais tempo do que Updateacelerar.
Luaan
9

Em caso de dúvida, use as melhores práticas.

Você está em dúvida.


Essa foi a resposta fácil. A realidade é mais complexa, é claro.

Primeiro, existe o mito de que a programação de jogos é uma programação de desempenho super super alto e tudo tem que ser o mais rápido possível. Classifico a programação de jogos como uma programação que reconhece o desempenho . O desenvolvedor deve estar ciente das restrições de desempenho e trabalhar dentro delas.

Segundo, existe o mito de que tornar tudo o mais rápido possível é o que faz um programa rodar mais rápido. Na realidade, identificar e otimizar gargalos é o que torna um programa mais rápido, e isso é muito mais fácil / mais barato / mais rápido, se o código é fácil de manter e modificar; código extremamente otimizado é difícil de manter e modificar. Daqui resulta que , para escrever programas extremamente otimizados, você precisa usar a otimização extrema do código sempre que necessário e o mais raramente possível.

Terceiro, o benefício das propriedades e dos acessadores é que eles são robustos para serem alterados e, assim, facilitam a manutenção e a modificação do código - o que é algo que você precisa para escrever programas rápidos. Deseja lançar uma exceção sempre que alguém deseja definir seu valor como nulo? Use um setter. Deseja enviar uma notificação sempre que o valor for alterado? Use um setter. Deseja registrar o novo valor? Use um setter. Deseja fazer uma inicialização lenta? Use um getter.

E quarto, pessoalmente, não sigo as melhores práticas da Microsoft neste caso. Se eu precisar de uma propriedade com informações triviais e públicas getters e getters , basta usar um campo e simplesmente alterá-lo para uma propriedade quando necessário. No entanto, raramente preciso de uma propriedade tão trivial - é muito mais comum querer verificar condições de contorno ou tornar o setter privado.

Peter - Unban Robert Harvey
fonte
2

É muito fácil para o tempo de execução .net incorporar propriedades simples, e geralmente o faz. Porém, esse inlining é normalmente desativado por criadores de perfil, portanto, é fácil ser enganado e pensar que alterar uma propriedade pública para um campo público acelerará o software.

No código que é chamado por outro código que não será recompilado quando o código for recompilado, é recomendável usar propriedades, pois a alteração de um campo para uma propriedade é uma alteração de quebra. Mas, para a maioria dos códigos, além de poder definir um ponto de interrupção no get / set, nunca vi benefícios em usar uma propriedade simples em comparação com um campo público.

A principal razão pela qual usei propriedades em meus 10 anos como programador profissional de C # foi parar outros programadores me dizendo que eu não estava mantendo o padrão de codificação. Essa foi uma boa troca, já que quase nunca havia uma boa razão para não usar uma propriedade.

Ian Ringrose
fonte
2

Estruturas e campos simples facilitarão a interoperabilidade com algumas APIs não gerenciadas. Frequentemente, você descobrirá que a API de baixo nível deseja acessar os valores por referência, o que é bom para o desempenho (como evitamos uma cópia desnecessária). O uso de propriedades é um obstáculo para isso e, muitas vezes, as bibliotecas do wrapper fazem cópias para facilitar o uso e, às vezes, para segurança.

Por esse motivo, você geralmente obtém melhor desempenho com tipos de vetores e matrizes que não têm propriedades, mas campos nus.


As melhores práticas não estão criando no vácuo. Apesar de algum culto à carga, em geral as melhores práticas existem por um bom motivo.

Nesse caso, temos alguns:

  • Uma propriedade permite alterar a implementação sem alterar o código do cliente (em nível binário, é possível alterar um campo para uma propriedade sem alterar o código do cliente no nível de origem, no entanto, ele será compilado para algo diferente após a alteração) . Isso significa que, usando uma propriedade desde o início, o código que faz referência à sua não precisará ser recompilado apenas para alterar o que a propriedade faz internamente.

  • Se nem todos os valores possíveis dos campos do seu tipo forem estados válidos, você não deseja expô-los ao código do cliente que o modifica. Portanto, se algumas combinações de valores forem inválidas, você deseja manter os campos privados (ou internos).

Eu tenho dito o código do cliente. Isso significa código que chama no seu. Se você não está criando uma biblioteca (ou mesmo fazendo uma biblioteca, mas usando interno, em vez de público), geralmente pode se safar e ter uma boa disciplina. Nessa situação, a melhor prática de usar propriedades está presente para impedir que você atire no próprio pé. Além disso, é muito mais fácil raciocinar sobre o código se você puder ver todos os lugares onde um campo pode mudar em um único arquivo, em vez de precisar se preocupar com o que está sendo modificado ou não em outro lugar. De fato, as propriedades também são boas para colocar pontos de interrupção ao descobrir o que deu errado.


Sim, há valor em ver o que está sendo feito na indústria. No entanto, você tem motivação para ir contra as melhores práticas? ou você está apenas indo contra as melhores práticas - dificultando o raciocínio do código - apenas porque alguém fez isso? Ah, a propósito, "outros fazem isso" é como você inicia um culto à carga.


Então ... O seu jogo está lento? É melhor dedicar tempo para descobrir o gargalo e corrigi-lo, em vez de especular o que poderia ser. Você pode ter certeza de que o compilador fará muitas otimizações; por isso, é provável que você esteja procurando o problema errado.

Por outro lado, se você está decidindo o que fazer para começar, deve se preocupar com quais algoritmos e estruturas de dados primeiro, em vez de se preocupar com detalhes menores, como campos x propriedades.


Finalmente, você ganha alguma coisa contrariando as melhores práticas?

Para os detalhes do seu caso (Unity e Mono para Android), o Unity usa valores por referência? Caso contrário, ele copiará os valores de qualquer maneira, sem ganho de desempenho.

Se isso acontecer, se você estiver passando esses dados para uma API que leva ref. Faz sentido tornar o campo público ou você pode tornar o tipo capaz de chamar a API diretamente?

Sim, é claro, poderia haver otimizações que você poderia fazer usando estruturas com campos nus. Por exemplo, você os acessa com ponteiros Span<T> ou similar. Eles também são compactos na memória, facilitando a serialização para envio pela rede ou armazenamento permanente (e sim, são cópias).

Agora, você escolheu os algoritmos e estruturas corretos, se eles se tornarem um gargalo, então você decide qual é a melhor maneira de corrigi-lo ... que poderia ser estruturas com campos nus ou não. Você poderá se preocupar com isso se e quando acontecer. Enquanto isso, você pode se preocupar com assuntos mais importantes, como tornar um jogo bom ou divertido que vale a pena jogar.

Theraot
fonte
1

... minha impressão é que, no desenvolvimento de jogos, a menos que você tenha uma razão para fazer o contrário, tudo deve ser o mais rápido possível.

:

Estou ciente de que a otimização prematura é ruim, mas como eu disse, no desenvolvimento de jogos você não faz algo lento quando um esforço semelhante pode torná-lo rápido.

Você está certo de estar ciente de que a otimização prematura é ruim, mas isso é apenas metade da história (veja a citação completa abaixo). Mesmo no desenvolvimento de jogos, nem tudo precisa ser o mais rápido possível.

Primeiro faça funcionar. Somente então, otimize os bits que precisam. Isso não quer dizer que o primeiro rascunho possa ser confuso ou desnecessariamente ineficiente, mas que a otimização não é uma prioridade neste estágio.

" O verdadeiro problema é que os programadores passaram muito tempo se preocupando com a eficiência nos lugares errados e nos horários errados ; a otimização prematura é a raiz de todo mal (ou pelo menos a maior parte) na programação". Donald Knuth, 1974.

Vincent O'Sullivan
fonte
1

Para coisas básicas como essa, o otimizador de C # vai acertar de qualquer maneira. Se você se preocupa com isso (e se preocupa com mais de 1% do seu código, está fazendo errado ), um atributo foi criado para você. O atributo [MethodImpl(MethodImplOptions.AggressiveInlining)]aumenta muito a chance de um método ser incorporado (getters e setters de propriedades são métodos).

Joshua
fonte
0

A exposição de membros do tipo estrutura como propriedades, em vez de campos, geralmente resultará em baixo desempenho e semântica, especialmente nos casos em que as estruturas são realmente tratadas como estruturas, e não como "objetos peculiares".

Uma estrutura no .NET é uma coleção de campos gravados juntos e que, para alguns propósitos, podem ser tratados como uma unidade. O .NET Framework foi projetado para permitir que estruturas sejam usadas da mesma maneira que objetos de classe, e há momentos em que isso pode ser conveniente.

Grande parte dos conselhos que cercam as estruturas se baseia na noção de que os programadores desejam usar estruturas como objetos, e não como coleções de campos colados. Por outro lado, há muitos casos em que pode ser útil ter algo que se comporte como um monte de campos colados. Se é disso que se precisa, o conselho do .NET adicionará trabalho desnecessário para programadores e compiladores, produzindo desempenho e semântica piores do que simplesmente usar estruturas diretamente.

Os maiores princípios a serem considerados ao usar estruturas são:

  1. Não passe ou retorne estruturas por valor ou faça com que elas sejam copiadas desnecessariamente.

  2. Se o princípio 1 for respeitado, as estruturas grandes terão um desempenho tão bom quanto as pequenas.

Se Alphablobé uma estrutura que contém 26 públicas intcampos nomeados az, e uma propriedade getter abque retorna a soma dos seus ae bcampos, então, dado Alphablob[] arr; List<Alphablob> list;, o código int foo = arr[0].ab + list[0].ab;precisará ler campos ae bde arr[0], mas vai precisar de ler todos os 26 campos de list[0]mesmo que ele vai ignore todos, exceto dois. Se alguém quiser ter uma coleção genérica do tipo lista que possa funcionar eficientemente com uma estrutura semelhante alphaBlob, substitua o getter indexado por um método:

delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
actOnItem<TParam>(int index, ActByRef<T, TParam> proc, ref TParam param);

que então invocaria proc(ref backingArray[index], ref param);. Dada essa coleção, se alguém substituir sum = myCollection[0].ab;por

int result;
myCollection.actOnItem<int>(0, 
  ref (ref alphaBlob item, ref int dest)=>dest = item,
  ref result);

isso evitaria a necessidade de copiar partes do alphaBlobque não serão usadas no getter de propriedades. Como o delegado transmitido não acessa nada além de seus refparâmetros, o compilador pode transmitir um delegado estático.

Infelizmente, o .NET Framework não define nenhum dos tipos de delegados necessários para que isso funcione bem, e a sintaxe acaba sendo uma bagunça. Por outro lado, essa abordagem permite evitar a necessidade de copiar estruturas desnecessariamente, o que tornará prático executar ações no local em grandes estruturas armazenadas em matrizes, que é a maneira mais eficiente de acessar o armazenamento.

supercat
fonte
Olá, você postou sua resposta na página errada?
piojo
@pojo: Talvez eu devesse ter deixado mais claro que o objetivo da resposta é identificar uma situação em que o uso de propriedades em vez de campos é ruim e identificar como se pode obter as vantagens semânticas e de desempenho dos campos, enquanto ainda encapsula o acesso.
supercat
@piojo: Minha edição torna as coisas mais claras?
supercat
"precisará ler todos os 26 campos da lista [0], apesar de ignorar todos, exceto dois." o que? Você tem certeza? Você verificou o MSIL? O C # quase sempre passa os tipos de classe por referência.
pjc50
@ pjc50: A leitura de uma propriedade gera um resultado por valor. Se o getter indexado para liste o getter de propriedade para abestiverem alinhados, pode ser possível para um JITter reconhecer que apenas membros aeb são necessários, mas tenho conhecimento de qualquer versão jitter realmente fazendo essas coisas.
supercat