La recommandation de Microsoft d'utiliser les propriétés C # s'applique-t-elle au développement de jeux?

26

Je comprends que parfois vous avez besoin de propriétés, comme:

public int[] Transitions { get; set; }

ou:

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

Mais j'ai l'impression que dans le développement de jeux, à moins que vous n'ayez une raison de faire autrement, tout devrait être presque aussi rapide que possible. Mon impression des choses que j'ai lues est que Unity 3D n'est pas sûr de l'accès en ligne via les propriétés, donc l'utilisation d'une propriété entraînera un appel de fonction supplémentaire.

Ai-je raison d'ignorer constamment les suggestions de Visual Studio de ne pas utiliser les champs publics? (Notez que nous utilisons int Foo { get; private set; }là où il y a un avantage à montrer l'utilisation prévue.)

Je suis conscient que l'optimisation prématurée est mauvaise, mais comme je l'ai dit, dans le développement de jeux, vous ne ralentissez pas lorsqu'un effort similaire pourrait le rendre rapide.

piojo
la source
34
Vérifiez toujours avec un profileur avant de croire que quelque chose est "lent". On m'a dit que quelque chose était "lent" et le profileur m'a dit qu'il devait exécuter 500 000 000 fois pour perdre une demi-seconde. Comme il ne s'exécuterait jamais plus de quelques centaines de fois dans un cadre, j'ai décidé que ce n'était pas pertinent.
Almo
5
J'ai trouvé qu'un peu d'abstraction ici et là aide à appliquer plus largement les optimisations de bas niveau. Par exemple, j'ai vu de nombreux projets C simples utilisant différents types de listes chaînées, qui sont terriblement lents par rapport aux tableaux contigus (par exemple le support de ArrayList). Cela est dû au fait que C n'a pas de génériques, et ces programmeurs C ont probablement trouvé plus facile de réimplémenter à plusieurs reprises des listes liées que de faire de même pour ArrayListsl'algorithme de redimensionnement de. Si vous trouvez une bonne optimisation de bas niveau, encapsulez-la!
hegel5000
3
@Almo Appliquez-vous la même logique aux opérations qui se produisent dans 10 000 endroits différents? Parce que c'est l'accès aux membres de la classe. Logiquement, vous devriez multiplier le coût des performances par 10K avant de décider que la classe d'optimisation n'a pas d'importance. Et 0,5 ms est une meilleure règle de base pour quand les performances de votre jeu seront ruinées, pas une demi-seconde. Votre argument est toujours valable, même si je ne pense pas que la bonne action soit aussi claire que vous le dites.
piojo
5
Vous avez 2 chevaux. Faites la course.
Mast
@piojo non. Il faut toujours penser à ces choses. Dans mon cas, c'était pour les boucles dans le code de l'interface utilisateur. Une instance d'interface utilisateur, quelques boucles, quelques itérations. Pour mémoire, j'ai voté pour votre commentaire. :)
Almo

Réponses:

44

Mais j'ai l'impression que dans le développement de jeux, à moins que vous n'ayez une raison de faire autrement, tout devrait être presque aussi rapide que possible.

Pas nécessairement. Tout comme dans les logiciels d'application, il y a du code dans un jeu qui est critique pour les performances et du code qui ne l'est pas.

Si le code est exécuté plusieurs milliers de fois par trame, une telle optimisation de bas niveau peut avoir un sens. (Bien que vous souhaitiez d'abord vérifier s'il est réellement nécessaire de l'appeler aussi souvent. Le code le plus rapide est le code que vous n'exécutez pas)

Si le code est exécuté une fois toutes les deux secondes, la lisibilité et la maintenabilité sont bien plus importantes que les performances.

J'ai lu que Unity 3D n'est pas sûr de l'accès en ligne via les propriétés, donc l'utilisation d'une propriété entraînera un appel de fonction supplémentaire.

Avec des propriétés triviales dans le style de int Foo { get; private set; }vous pouvez être certain que le compilateur optimisera le wrapper de propriété. Mais:

  • si vous avez réellement une implémentation, vous ne pouvez plus en être sûr. Plus cette implémentation est complexe, moins il est probable que le compilateur sera en mesure de l'intégrer.
  • Les propriétés peuvent masquer la complexité. Il s'agit d'une épée à double tranchant. D'une part, cela rend votre code plus lisible et modifiable. Mais d'un autre côté, un autre développeur qui accède à une propriété de votre classe peut penser qu'il accède à une seule variable et ne se rend pas compte de la quantité de code que vous avez réellement cachée derrière cette propriété. Quand une propriété commence à devenir coûteuse en termes de calcul, j'ai tendance à la refactoriser en une méthode explicite GetFoo()/ SetFoo(value): pour impliquer qu'il se passe beaucoup plus de choses en arrière-plan. (ou si je dois être encore plus sûr que d' autres obtiennent l'indice: CalculateFoo()/ SwitchFoo(value))
Philipp
la source
L'accès à un champ dans un champ public de type structure en lecture seule est rapide, même si ce champ se trouve dans une structure qui est dans une structure etc., à condition que l'objet de niveau supérieur soit soit une classe non en lecture seule ou une variable autonome non en lecture seule. Les structures peuvent être assez grandes sans nuire aux performances si ces conditions s'appliquent. Briser ce modèle entraînera un ralentissement proportionnel à la taille de la structure.
supercat
5
"alors j'ai tendance à le refactoriser en une méthode GetFoo () / SetFoo (value) explicite:" - Bon point, j'ai tendance à déplacer les opérations lourdes des propriétés vers les méthodes.
Mark Rogers
Existe-t-il un moyen de confirmer que le compilateur Unity 3D effectue l'optimisation que vous mentionnez (dans les versions IL2CPP et Mono pour Android)?
piojo
9
@piojo Comme pour la plupart des questions de performances, la réponse la plus simple est "faites-le". Vous avez le code prêt - testez la différence entre avoir un champ et avoir une propriété. Les détails de l'implémentation ne valent la peine d'être étudiés que lorsque vous pouvez réellement mesurer une différence importante entre les deux approches. D'après mon expérience, si vous écrivez tout pour être aussi rapide que possible, vous limitez les optimisations de haut niveau disponibles et essayez simplement de parcourir les parties de bas niveau ("ne peut pas voir la forêt pour les arbres"). Par exemple, trouver des moyens de sauter Updateentièrement vous fera gagner plus de temps que de faire Updatevite.
Luaan
9

En cas de doute, appliquez les meilleures pratiques.

Vous avez un doute.


C'était la réponse facile. La réalité est bien sûr plus complexe.

Tout d'abord, il y a le mythe selon lequel la programmation de jeux est une programmation ultra haute performance et tout doit être aussi rapide que possible. Je classe la programmation de jeux comme une programmation sensible aux performances . Le développeur doit être conscient des contraintes de performances et y travailler.

Deuxièmement, il y a le mythe selon lequel rendre chaque chose aussi rapide que possible est ce qui fait qu'un programme s'exécute rapidement. En réalité, identifier et optimiser les goulots d'étranglement est ce qui rend un programme rapide, et c'est beaucoup plus facile / moins cher / plus rapide à faire si le code est facile à maintenir et à modifier; le code extrêmement optimisé est difficile à maintenir et à modifier. Il s'ensuit que pour écrire des programmes extrêmement optimisés, vous devez utiliser une optimisation de code extrême aussi souvent que nécessaire et aussi rarement que possible.

Troisièmement, l'avantage des propriétés et des accesseurs est qu'ils sont robustes à changer et rendent ainsi le code plus facile à maintenir et à modifier - ce dont vous avez besoin pour écrire des programmes rapides. Vous souhaitez lever une exception chaque fois que quelqu'un souhaite définir votre valeur sur null? Utilisez un passeur. Vous souhaitez envoyer une notification chaque fois que la valeur change? Utilisez un passeur. Vous voulez enregistrer la nouvelle valeur? Utilisez un passeur. Vous voulez faire une initialisation paresseuse? Utilisez un getter.

Et quatrièmement, personnellement, je ne respecte pas les meilleures pratiques de Microsoft dans ce cas. Si j'ai besoin d'une propriété avec des getters et setters triviaux et publics , j'utilise simplement un champ, et je le change simplement en propriété quand j'en ai besoin. Cependant, j'ai très rarement besoin d'une propriété aussi triviale - il est beaucoup plus courant que je veuille vérifier les conditions aux limites, ou que je veuille rendre le setter privé.

Peter - Unban Robert Harvey
la source
2

Il est très facile pour le runtime .net d'incorporer des propriétés simples, et c'est souvent le cas. Mais, cette inline est normalement désactivée par les profileurs, il est donc facile d'être induit en erreur et de penser que le changement d'une propriété publique en un domaine public accélérera le logiciel.

Dans le code qui est appelé par un autre code qui ne sera pas recompilé lorsque votre code est recompilé, il y a de bonnes raisons d'utiliser des propriétés, car le passage d'un champ à une propriété est un changement de rupture. Mais pour la plupart du code, outre la possibilité de définir un point d'arrêt sur le get / set, je n'ai jamais vu d'avantages à utiliser une propriété simple par rapport à un champ public.

La principale raison pour laquelle j'ai utilisé des propriétés au cours de mes 10 années en tant que programmeur C # professionnel était d'empêcher d'autres programmeurs de me dire que je ne respectais pas la norme de codage. C'était un bon compromis, car il n'y avait presque jamais de bonne raison de ne pas utiliser une propriété.

Ian Ringrose
la source
2

Les structures et les champs nus faciliteront l'interopérabilité avec certaines API non gérées. Souvent, vous constaterez que l'API de bas niveau veut accéder aux valeurs par référence, ce qui est bon pour les performances (car nous évitons une copie inutile). L'utilisation de propriétés est un obstacle à cela, et souvent les bibliothèques d'encapsuleurs font des copies pour la facilité d'utilisation et parfois pour la sécurité.

De ce fait, vous obtiendrez souvent de meilleures performances avec des types de vecteurs et de matrices qui n'ont pas de propriétés mais des champs nus.


Les meilleures pratiques ne se créent pas sous vide. Malgré un certain culte du fret, en général, les meilleures pratiques sont là pour une bonne raison.

Dans ce cas, nous avons un couple:

  • Une propriété vous permet de modifier l'implémentation sans changer le code client (au niveau binaire, il est possible de changer un champ en une propriété sans changer le code client au niveau source, mais il se compilera en quelque chose de différent après le changement) . Cela signifie qu'en utilisant une propriété depuis le début, le code qui fait référence au vôtre n'aura pas à être recompilé juste pour changer ce que fait la propriété en interne.

  • Si toutes les valeurs possibles des champs de votre type ne sont pas des états valides, vous ne souhaitez pas les exposer au code client qui le modifiera. Ainsi, si certaines combinaisons de valeurs ne sont pas valides, vous souhaitez conserver les champs privés (ou internes).

J'ai dit le code client. Cela signifie un code qui appelle le vôtre. Si vous ne faites pas de bibliothèque (ou même si vous faites de la bibliothèque mais que vous utilisez interne au lieu de public), vous pouvez généralement vous en tirer avec une bonne discipline. Dans cette situation, la meilleure pratique d'utilisation des propriétés est de vous empêcher de vous tirer une balle dans le pied. De plus, il est beaucoup plus facile de raisonner sur le code si vous pouvez voir tous les endroits où un champ peut changer dans un seul fichier, au lieu d'avoir à vous soucier de tout ce qui est en train d'être modifié ailleurs. En fait, les propriétés sont également bonnes pour mettre des points d'arrêt lorsque vous déterminez ce qui ne va pas.


Oui, il est utile de voir ce qui se fait dans l'industrie. Cependant, avez-vous une motivation pour aller à l'encontre des meilleures pratiques? ou allez-vous simplement à l'encontre des meilleures pratiques - rendant le code plus difficile à raisonner - simplement parce que quelqu'un d'autre l'a fait? Ah, au fait, "les autres le font", c'est comme ça que vous démarrez un culte du cargo.


Alors ... Votre jeu tourne-t-il lentement? Il vaut mieux consacrer du temps à trouver le goulot d'étranglement et à le corriger, au lieu de spéculer sur ce que cela pourrait être. Vous pouvez être assuré que le compilateur fera beaucoup d'optimisations, à cause de cela, il est probable que vous cherchiez le mauvais problème.

D'un autre côté, si vous décidez quoi faire pour commencer, vous devez d'abord vous soucier des algorithmes et des structures de données, au lieu de vous soucier de petits détails tels que les champs par rapport aux propriétés.


Enfin, gagnez-vous quelque chose en allant à l'encontre des meilleures pratiques?

Pour les particularités de votre cas (Unity et Mono pour Android), Unity prend-il des valeurs par référence? Si ce n'est pas le cas, il copiera les valeurs de toute façon, aucun gain de performances là-bas.

Si c'est le cas, si vous transmettez ces données à une API qui prend ref. Est-il judicieux de rendre le champ public, ou vous pourriez rendre le type capable d'appeler directement l'API?

Oui, bien sûr, il pourrait y avoir des optimisations que vous pourriez faire en utilisant des structures avec des champs nus. Par exemple, vous y accédez avec des pointeurs Span<T> ou similaires. Ils sont également compacts en mémoire, ce qui les rend faciles à sérialiser pour envoyer sur le réseau ou à mettre en stockage permanent (et oui, ce sont des copies).

Maintenant, avez-vous choisi les bons algorithmes et structures, s'ils se révèlent être un goulot d'étranglement, alors vous décidez quelle est la meilleure façon de le corriger ... qui pourrait être des structures avec des champs nus ou non. Vous pourrez vous en soucier si et quand cela se produit. Pendant ce temps, vous pouvez vous soucier de questions plus importantes telles que faire un bon jeu amusant.

Theraot
la source
1

... mon impression est que dans le développement de jeux, à moins que vous n'ayez une raison de faire autrement, tout devrait être presque aussi rapide que possible.

:

Je suis conscient que l'optimisation prématurée est mauvaise, mais comme je l'ai dit, dans le développement de jeux, vous ne ralentissez pas lorsqu'un effort similaire pourrait le rendre rapide.

Vous avez raison de savoir qu'une optimisation prématurée est mauvaise, mais ce n'est que la moitié de l'histoire (voir la citation complète ci-dessous). Même dans le développement de jeux, tout ne doit pas être aussi rapide que possible.

Faites-le d'abord fonctionner. Alors seulement, optimisez les bits qui en ont besoin. Cela ne veut pas dire que le premier projet peut être désordonné ou inefficace, mais que l'optimisation n'est pas une priorité à ce stade.

" Le vrai problème est que les programmeurs ont passé beaucoup trop de temps à se soucier de l'efficacité aux mauvais endroits et aux mauvais moments . L' optimisation prématurée est à l'origine de tous les maux (ou du moins la plupart) de la programmation." Donald Knuth, 1974.

Vincent O'Sullivan
la source
1

Pour des trucs de base comme ça, l'optimiseur C # va bien faire les choses de toute façon. Si vous vous en souciez (et si vous vous en souciez pour plus de 1% de votre code, vous vous trompez ), un attribut a été créé pour vous. L'attribut [MethodImpl(MethodImplOptions.AggressiveInlining)]augmente considérablement les chances qu'une méthode soit alignée (les getters et setters de propriété sont des méthodes).

Joshua
la source
0

L'exposition de membres de type structure en tant que propriétés plutôt que champs entraînera souvent de mauvaises performances et une sémantique médiocre, en particulier dans les cas où les structures sont réellement traitées comme des structures plutôt que comme des "objets originaux".

Une structure en .NET est une collection de champs collés ensemble et qui, à certaines fins, peuvent être traités comme une unité. Le .NET Framework est conçu pour permettre aux structures d'être utilisées de la même manière que les objets de classe, et il peut parfois être pratique.

La plupart des conseils concernant les structures reposent sur l'idée que les programmeurs voudront utiliser les structures comme des objets, plutôt que comme des collections de champs collées. D'un autre côté, il existe de nombreux cas où il peut être utile d'avoir quelque chose qui se comporte comme un ensemble de champs collés. Si c'est ce dont on a besoin, les conseils .NET ajouteront du travail inutile aux programmeurs et aux compilateurs, ce qui donnera des performances et une sémantique moins bonnes que la simple utilisation directe de structures.

Les plus grands principes à garder à l'esprit lors de l'utilisation de structures sont:

  1. Ne passez pas ou ne renvoyez pas les structures par valeur ou ne les faites pas copier inutilement.

  2. Si le principe n ° 1 est respecté, les grandes structures fonctionneront aussi bien que les petites.

Si Alphablobest une structure contenant 26 publics intchamps nommés az, et un getter de la propriété abqui retourne la somme de ses aet bchamps, puis donné Alphablob[] arr; List<Alphablob> list;, le code int foo = arr[0].ab + list[0].ab;devra lire des champs aet bde arr[0], mais devra lire tous les 26 domaines de la list[0]même si elle ignorer tous sauf deux. Si l'on voulait avoir une collection de type liste générique qui pourrait fonctionner efficacement avec une structure comme alphaBlob, on devrait remplacer le getter indexé par une méthode:

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

qui invoquerait alors proc(ref backingArray[index], ref param);. Compte tenu d'une telle collection, si l'on remplace sum = myCollection[0].ab;par

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

cela éviterait de devoir copier des parties de la alphaBlobqui ne seront pas utilisées dans le getter de propriété. Étant donné que le délégué passé n'accède à rien d'autre que ses refparamètres, le compilateur peut passer un délégué statique.

Malheureusement, le .NET Framework ne définit aucun des types de délégués nécessaires pour que cette chose fonctionne correctement, et la syntaxe finit par être un peu un gâchis. D'un autre côté, cette approche permet d'éviter de copier inutilement des structures, ce qui à son tour rendra pratique l'exécution d'actions sur place sur de grandes structures stockées dans des tableaux, ce qui est le moyen le plus efficace d'accéder au stockage.

supercat
la source
Bonjour, avez-vous posté votre réponse sur la mauvaise page?
piojo
@pojo: J'aurais peut-être dû préciser que le but de la réponse est d'identifier une situation où l'utilisation de propriétés plutôt que de champs est mauvaise, et d'identifier comment on peut obtenir les performances et les avantages sémantiques des champs, tout en encapsulant l'accès.
supercat
@piojo: Ma modification rend-elle les choses plus claires?
supercat
"devra lire les 26 champs de la liste [0] même s'il les ignorera tous sauf deux." quelle? Êtes-vous sûr? Avez-vous vérifié le MSIL? C # passe presque toujours les types de classe par référence.
pjc50
@ pjc50: La lecture d'une propriété donne un résultat par valeur. Si le getter indexé pour listet le getter de propriété pour absont tous deux en ligne, il peut être possible pour un JITter de reconnaître que seuls les membres aet bsont nécessaires, mais je ne suis au courant d'aucune version de JITter faisant de telles choses.
supercat