La raccomandazione di Microsoft di utilizzare le proprietà C # è applicabile allo sviluppo del gioco?


26

Capisco che a volte hai bisogno di proprietà, come:

public int[] Transitions { get; set; }

o:

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

Ma la mia impressione è che nello sviluppo del gioco, a meno che tu non abbia una ragione per fare diversamente, tutto dovrebbe essere quasi il più veloce possibile. La mia impressione dalle cose che ho letto è che Unity 3D non è sicuro di incorporare l'accesso tramite le proprietà, quindi l'utilizzo di una proprietà comporterà una chiamata di funzione aggiuntiva.

Ho ragione a ignorare costantemente i suggerimenti di Visual Studio di non utilizzare i campi pubblici? (Si noti che utilizziamo int Foo { get; private set; }laddove vi sia un vantaggio nel mostrare l'uso previsto.)

Sono consapevole che l'ottimizzazione prematura è negativa, ma come ho detto, nello sviluppo del gioco non fai qualcosa di lento quando uno sforzo simile potrebbe farlo velocemente.


34
Verificare sempre con un profiler prima di ritenere che qualcosa sia "lento". Mi è stato detto che qualcosa era "lento" e il profiler mi ha detto che doveva eseguire 500.000.000 di volte per perdere mezzo secondo. Dato che non sarebbe mai stato eseguito più di qualche centinaio di volte in un frame, ho deciso che era irrilevante.
Almo,

5
Ho scoperto che un po 'di astrazione qua e là aiuta ad applicare le ottimizzazioni di basso livello più ampiamente. Ad esempio, ho visto numerosi progetti C semplici che utilizzano vari tipi di elenchi collegati, che sono terribilmente lenti rispetto ad array contigui (ad esempio il supporto per ArrayList). Questo perché C non ha generici e probabilmente questi programmatori C hanno trovato più facile reimplementare ripetutamente le liste collegate piuttosto che fare lo stesso per ArrayListsl'algoritmo di ridimensionamento. Se trovi una buona ottimizzazione di basso livello, incapsulala!
hegel5000,

3
@Almo Applicate la stessa logica alle operazioni che si verificano in 10.000 luoghi diversi? Perché quello è l'accesso dei membri della classe. Logicamente dovresti moltiplicare il costo delle prestazioni per 10K prima di decidere che la classe di ottimizzazione non ha importanza. E 0,5 ms è una regola empirica migliore per quando le prestazioni del tuo gioco saranno rovinate, non per mezzo secondo. Il tuo punto è ancora valido, anche se non credo che l'azione giusta sia chiara come si immagina.
Piojo,

5
Hai 2 cavalli. Race loro.
Albero

@piojo Io no. Qualche pensiero deve sempre andare in queste cose. Nel mio caso, era per i loop nel codice dell'interfaccia utente. Un'istanza dell'interfaccia utente, pochi loop, poche iterazioni. Per la cronaca, ho annullato il tuo commento. :)
Almo,

Risposte:


44

Ma la mia impressione è che nello sviluppo del gioco, a meno che tu non abbia una ragione per fare diversamente, tutto dovrebbe essere quasi il più veloce possibile.

Non necessariamente. Proprio come nel software applicativo, c'è un codice in un gioco che è critico per le prestazioni e un codice che non lo è.

Se il codice viene eseguito diverse migliaia di volte per frame, un'ottimizzazione di basso livello potrebbe avere senso. (Anche se potresti voler prima verificare se è effettivamente necessario chiamarlo così spesso. Il codice più veloce è il codice che non esegui)

Se il codice viene eseguito una volta ogni due secondi, la leggibilità e la manutenibilità sono molto più importanti delle prestazioni.

Ho letto è che Unity 3D non è sicuro di incorporare l'accesso tramite le proprietà, quindi l'utilizzo di una proprietà comporterà una chiamata di funzione aggiuntiva.

Con proprietà insignificanti nello stile di int Foo { get; private set; }te puoi essere abbastanza sicuro che il compilatore ottimizzerà il wrapper di proprietà. Ma:

  • se in realtà hai un'implementazione, non puoi più esserne sicuro. Più complessa è l'implementazione, meno è probabile che il compilatore sarà in grado di incorporarlo.
  • Le proprietà possono nascondere la complessità. Questa è un'arma a doppio taglio. Da un lato, rende il tuo codice più leggibile e modificabile. D'altra parte, un altro sviluppatore che accede a una proprietà della tua classe potrebbe pensare che stiano solo accedendo a una singola variabile e non si rendano conto di quanto codice hai effettivamente nascosto dietro quella proprietà. Quando una proprietà inizia a diventare computazionalmente costosa, allora tendo a trasformarla in un metodo GetFoo()/ esplicito SetFoo(value): implicare che ci sia molto di più dietro le quinte. (o se devo essere ancora più sicuro che altri abbiano il suggerimento: CalculateFoo()/ SwitchFoo(value))

L'accesso a un campo all'interno di un campo pubblico di sola lettura del tipo di struttura è rapido, anche se quel campo si trova all'interno di una struttura che si trova all'interno di una struttura ecc., A condizione che l'oggetto di livello superiore sia una classe di sola lettura campo o una variabile indipendente di sola lettura. Le strutture possono essere piuttosto grandi senza influire negativamente sulle prestazioni se si applicano queste condizioni. La rottura di questo modello causerà un rallentamento proporzionale alla dimensione della struttura.
supercat

5
"poi tendo a trasformarlo in un metodo GetFoo () / SetFoo (valore) esplicito:" - Bene, tendo a spostare operazioni pesanti dalle proprietà ai metodi.
Mark Rogers,

C'è un modo per confermare che il compilatore Unity 3D apporta l'ottimizzazione di cui parli (nelle build IL2CPP e Mono per Android)?
Piojo,

9
@piojo Come per la maggior parte delle domande sulle prestazioni, la risposta più semplice è "fallo". Hai il codice pronto: prova la differenza tra avere un campo e avere una proprietà. Vale la pena investigare i dettagli dell'implementazione solo quando si può effettivamente misurare una differenza importante tra i due approcci. Nella mia esperienza, se scrivi tutto il più velocemente possibile, limiti le ottimizzazioni di alto livello disponibili e provi semplicemente a correre le parti di basso livello ("non riesco a vedere la foresta per gli alberi"). Ad esempio, trovare modi per saltare del Updatetutto ti farà risparmiare più tempo che Updateaccelerare.
Luaan,

9

In caso di dubbi, utilizzare le migliori pratiche.

Sei in dubbio


Questa è stata la risposta facile. La realtà è più complessa, ovviamente.

Innanzitutto, c'è il mito che la programmazione del gioco è una programmazione ad altissime prestazioni e che tutto deve essere il più veloce possibile. Classifico la programmazione di gioco come programmazione attenta alle prestazioni . Lo sviluppatore deve essere consapevole dei vincoli di prestazione e operare al loro interno.

Secondo, c'è il mito che rendere ogni singola cosa il più veloce possibile è ciò che rende veloce l'esecuzione di un programma. In realtà, identificare e ottimizzare i colli di bottiglia è ciò che rende veloce un programma, ed è molto più facile / economico / veloce da fare se il codice è facile da mantenere e modificare; codice estremamente ottimizzato è difficile da mantenere e modificare. Ne consegue che per scrivere programmi estremamente ottimizzati, è necessario utilizzare l'ottimizzazione del codice estrema il più spesso necessario e il più raramente possibile.

In terzo luogo, il vantaggio delle proprietà e degli accessori è che sono robusti da cambiare e quindi semplificano la manutenzione e la modifica del codice, il che è necessario per scrivere programmi veloci. Vuoi generare un'eccezione ogni volta che qualcuno vuole impostare il valore su null? Usa un setter. Vuoi inviare una notifica ogni volta che il valore cambia? Usa un setter. Vuoi registrare il nuovo valore? Usa un setter. Vuoi fare l'inizializzazione pigra? Usa un getter.

E in quarto luogo, personalmente non seguo le migliori pratiche di Microsoft in questo caso. Se ho bisogno di una proprietà con getter e setter banali e pubblici , uso semplicemente un campo e lo cambio semplicemente in una proprietà quando ne ho bisogno. Tuttavia, molto raramente ho bisogno di una proprietà così banale: è molto più comune che io voglia verificare le condizioni al contorno o rendere privato il setter.


2

È molto facile per il runtime .net incorporare proprietà semplici, e spesso lo fa. Ma questo allineamento è normalmente disabilitato dai profiler, quindi è facile essere fuorviato e pensare che cambiare una proprietà pubblica in un campo pubblico accelererà il software.

Nel codice che viene chiamato da un altro codice che non verrà ricompilato quando il codice viene ricompilato, è opportuno utilizzare le proprietà, poiché il passaggio da un campo a una proprietà è una modifica non definitiva. Ma per la maggior parte del codice, oltre a essere in grado di impostare un breakpoint su get / set, non ho mai visto vantaggi nell'usare una proprietà semplice rispetto a un campo pubblico.

Il motivo principale per cui ho usato le proprietà nei miei 10 anni come programmatore professionista C # è stato quello di impedire ad altri programmatori di dirmi che non stavo rispettando lo standard di codifica. Questo è stato un buon compromesso, come quasi mai esisteva una buona ragione per non usare una proprietà.


2

Strutture e campi nudi faciliteranno l'interoperabilità con alcune API non gestite. Spesso troverai che l'API di basso livello vuole accedere ai valori per riferimento, il che è buono per le prestazioni (poiché evitiamo una copia non necessaria). L'uso delle proprietà è un ostacolo a ciò, e spesso le librerie wrapper eseguono copie per facilità d'uso e talvolta per sicurezza.

Per questo motivo, otterrai spesso prestazioni migliori con tipi di vettori e matrici che non hanno proprietà ma campi nudi.


Le migliori pratiche non stanno creando nel vaccum. Nonostante un po 'di culto delle merci, in genere le migliori pratiche esistono per una buona ragione.

In questo caso, ne abbiamo un paio:

  • Una proprietà consente di modificare l'implementazione senza modificare il codice client (a livello binario, è possibile modificare un campo in una proprietà senza modificare il codice client a livello di origine, tuttavia verrà compilato in qualcosa di diverso dopo la modifica) . Ciò significa che usando una proprietà dall'inizio, il codice che fa riferimento al tuo non dovrà essere ricompilato solo per cambiare ciò che la proprietà fa internamente.

  • Se non tutti i possibili valori dei campi del tipo sono stati validi, non si desidera esporli al codice client che il codice lo modifica. Pertanto, se alcune combinazioni di valori non sono valide, si desidera mantenere i campi privati ​​(o interni).

Ho detto il codice client. Ciò significa che il codice che chiama nel tuo. Se non stai creando una libreria (o addirittura non stai creando una libreria ma usi interni invece di pubblici), di solito puoi cavartela e avere una buona disciplina. In quella situazione, la migliore pratica di usare le proprietà è lì per impedirti di spararti nel piede. Inoltre, è molto più facile ragionare sul codice se puoi vedere tutti i luoghi in cui un campo può cambiare in un singolo file, invece di doverti preoccupare di qualunque cosa venga modificata da qualche altra parte. In effetti, le proprietà sono anche buone per mettere dei punti di interruzione quando si capisce cosa è andato storto.


Sì, c'è valore è vedere cosa si sta facendo nell'industria. Tuttavia, hai una motivazione per andare contro le migliori pratiche? o stai solo andando contro le migliori pratiche - rendendo il codice più difficile da ragionare - solo perché lo ha fatto qualcun altro? Ah, a proposito, "altri lo fanno" è il modo in cui inizi un culto del carico.


Quindi ... Il tuo gioco è lento? È meglio dedicare tempo a capire il collo di bottiglia e risolverlo, invece di speculare su cosa potrebbe essere. Puoi stare certo che il compilatore farà molte ottimizzazioni, per questo motivo è probabile che tu stia esaminando il problema sbagliato.

Il rovescio della medaglia, se stai decidendo cosa fare per cominciare, dovresti preoccuparti di quali algoritmi e strutture dati prima, invece di preoccuparti di dettagli più piccoli come campi vs proprietà.


Infine, guadagni qualcosa andando contro le migliori pratiche?

Per i dettagli del tuo caso (Unity e Mono per Android), Unity assume valori come riferimento? In caso contrario, copierà comunque i valori, senza alcun guadagno in termini di prestazioni.

In tal caso, se si stanno passando questi dati a un'API che accetta ref. Ha senso rendere pubblico il campo o potresti rendere il tipo in grado di chiamare direttamente l'API?

Sì, certo, ci potrebbero essere delle ottimizzazioni che potresti fare usando le strutture con campi nudi. Ad esempio, puoi accedervi con i puntatori Span<T> o simili. Sono anche compatti in memoria, il che li rende facili da serializzare per l'invio in rete o l'archiviazione permanente (e sì, quelli sono copie).

Ora, hai scelto gli algoritmi e le strutture giuste, se si rivelano un collo di bottiglia, quindi decidi qual è il modo migliore per risolverlo ... che potrebbe essere strutture con campi nudi o no. Sarai in grado di preoccuparti di questo se e quando accadrà. Nel frattempo puoi preoccuparti di questioni più importanti come fare un gioco buono o divertente che valga la pena giocare.


1

... la mia impressione è che nello sviluppo del gioco, a meno che tu non abbia una ragione per fare diversamente, tutto dovrebbe essere quasi il più veloce possibile.

:

Sono consapevole che l'ottimizzazione prematura è negativa, ma come ho detto, nello sviluppo del gioco non fai qualcosa di lento quando uno sforzo simile potrebbe farlo velocemente.

Hai ragione a essere consapevole del fatto che l'ottimizzazione prematura è cattiva, ma questa è solo metà della storia (vedi la citazione completa di seguito). Anche nello sviluppo del gioco, non tutto deve essere il più veloce possibile.

Per prima cosa fallo funzionare. Solo allora, ottimizza i bit che ne hanno bisogno. Questo non vuol dire che la prima bozza possa essere disordinata o inutilmente inefficiente ma che l'ottimizzazione non è una priorità in questa fase.

" Il vero problema è che i programmatori hanno trascorso troppo tempo a preoccuparsi dell'efficienza nei posti sbagliati e nei momenti sbagliati ; l'ottimizzazione prematura è la radice di tutti i mali (o almeno la maggior parte di essi) nella programmazione." Donald Knuth, 1974.


1

Per cose di base come quelle, l'ottimizzatore C # lo farà comunque bene. Se ti preoccupi (e se ti preoccupi per oltre l'1% del tuo codice, stai sbagliando ) è stato creato un attributo per te. L'attributo [MethodImpl(MethodImplOptions.AggressiveInlining)]aumenta notevolmente la possibilità che un metodo sia incorporato (i getter e setter di proprietà sono metodi).


0

Esporre i membri del tipo di struttura come proprietà anziché come campi spesso produrrà scarse prestazioni e semantica, specialmente nei casi in cui le strutture vengono effettivamente trattate come strutture, piuttosto che come "oggetti stravaganti".

Una struttura in .NET è una raccolta di campi uniti insieme e che per alcuni scopi possono essere trattati come un'unità. .NET Framework è progettato per consentire alle strutture di essere utilizzate più o meno allo stesso modo degli oggetti di classe e in alcuni casi può essere utile.

Gran parte del consiglio che circonda le strutture è basato sull'idea che i programmatori vorranno usare le strutture come oggetti, piuttosto che come raccolte di campi collegati tra loro. D'altra parte, ci sono molti casi in cui può essere utile avere qualcosa che si comporta come un insieme di campi collegati. Se questo è ciò di cui si ha bisogno, i consigli di .NET aggiungeranno lavoro inutile sia per programmatori che per compilatori, producendo prestazioni e semantica peggiori rispetto al semplice utilizzo diretto delle strutture.

I più grandi principi da tenere a mente quando si usano le strutture sono:

  1. Non passare o restituire le strutture in base al valore o altrimenti farle copiare inutilmente.

  2. Se il principio n. 1 viene rispettato, le strutture di grandi dimensioni funzioneranno altrettanto bene di quelle piccole.

Se Alphablobè una struttura che contiene 26 intcampi pubblici di nome az e un getter di proprietà abche restituisce la somma dei suoi campi ae b, quindi dato Alphablob[] arr; List<Alphablob> list;, il codice int foo = arr[0].ab + list[0].ab;dovrà leggere i campi ae bdi arr[0], ma dovrà leggere tutti i 26 campi di list[0]anche se sarà ignorali tutti tranne due. Se si voleva avere una raccolta di tipo elenco generico che potesse funzionare in modo efficiente con una struttura simile alphaBlob, si dovrebbe sostituire il getter indicizzato con un metodo:

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

che poi invocherebbe proc(ref backingArray[index], ref param);. Data una tale raccolta, se si sostituisce sum = myCollection[0].ab;con

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

ciò eviterebbe la necessità di copiare parti di alphaBlobciò che non verranno utilizzate nel getter di proprietà. Dal momento che il delegato passato non accede ad altro che al suoref parametri, il compilatore può passare un delegato statico.

Sfortunatamente, .NET Framework non definisce nessuno dei tipi di delegati necessari per far funzionare bene questa cosa e la sintassi finisce per essere un po 'un casino. D'altro canto, questo approccio consente di evitare di copiare inutilmente le strutture, il che a sua volta renderà pratico l'esecuzione di azioni sul posto su grandi strutture archiviate all'interno di array, che è il modo più efficiente di accedere all'archiviazione.


Ciao, hai pubblicato la tua risposta nella pagina sbagliata?
Piojo,

@pojo: Forse avrei dovuto chiarire che lo scopo della risposta è identificare una situazione in cui l'uso delle proprietà anziché i campi è negativo e identificare come si possono ottenere le prestazioni e i vantaggi semantici dei campi, pur incapsulando ancora l'accesso.
supercat

@piojo: la mia modifica rende le cose più chiare?
supercat

"dovrà leggere tutti i 26 campi dell'elenco [0] anche se ignorerà tutti tranne due." che cosa? Sei sicuro? Hai controllato MSIL? C # passa quasi sempre i tipi di classe per riferimento.
pjc50,

@ pjc50: la lettura di una proprietà produce un risultato in base al valore. Se sia il getter indicizzato sia il getter listdelle proprietà absono entrambi allineati, potrebbe essere possibile per un JITter riconoscere che solo i membri ae bsono necessari, ma non sono a conoscenza di alcuna versione di JITter che sta effettivamente facendo tali cose.
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.