Feb 6, 2019

Éviter les copies de données cachées en C#

Read this post in English

Leia este post em Português

Je voulais remercier tout ceux qui m'ont aidé avec la correction de ce texte (sans ordre particulier) : Charles Beauchemin, Vladimir Nachbaur.


Est ce que vous voyez un problème dans l’extrait de code ci-dessous ? Indice : Il y en a au moins un, lié à l’utilisation de Value Types & la performance.


D’accord, rendons le problème un peu plus évident en introduisant une erreur, laquelle, en supposant que le code en question a des tests unitaires, fera probablement remarquer le comportement immédiatement:


Quel serait le résultat maintenant ?
Si vous avez répondu :
M() : 0
M() : 1
M() : 2

Vous serez probablement surpris de voir :

M() : 0
M() : 0
M() : 0

C'est maintenant évident que quelque chose n’est pas correct.

Le problème est que le champ s a eté déclaré readonly, ce qui fait que le compilateur publie des copies défensives avant chaque appel de méthode sur ce type (dans notre cas s.M()); ça signifie que la méthode M() va ajouter le champ counter en une copie de la structure originale (au lieu de s), ce qui explique le comportement observé.

Notez que dans la première version du code (dans lequel la structure n’est pas modifiable), il n'est pas facile de détecter que quelque chose n'est pas correcte. Même dans la dernière version du code ce n'est pas clair pourquoi on n’observe jamais l’incrément de s.counter.

Pour comprendre ce qui se passe, examinons le code IL généré pour la fonction Main() (vous pouvez utiliser le site sharplab.io pour expérimenter avec le code) :


Maintenant, c’est un peu plus facile de remarquer les copies supplémentaires qui se produisent dans les offsets IL_0001 & IL_0006, puis IL_000f & IL_0014, et enfin IL_001d & IL_0022. Notez qu’après la copie du contenu de s dans la variable située dans l’emplacement 1, cette adresse est chargée sur la pile et le méthode M() est appelée.

Malheureusement, avec C#, c’est relativement facile de tomber dans d’autres “pièges” ; le prochain exemple montre deux autres cas : les paramètres in et les variables locales ref readonly:

La question naturelle qui vient est : Est-ce que c’est possible de s’éviter cette copies ? Si oui, comment ? La réponse dépend de la version du langage C# que vous utilisez :
  • < 7.3: Le seul moyen que je connaisse est de supprimer le modificateur readonly du champ (s dans notre exemple)
  • >= 7.3: Vous pouvez déclarer sa struct comme readonly afin de s’assurer que la même n’est pas modifiable donc le compilateur n'a pas à émettre de copies défensives (au cas où vous essaierait de modifier l’état de la structure, le compilateur émettra une erreur).
Il reste une question : à quel point ces copies sont-elles mauvaises? À mon avis la réponse dépend de comment elles affectent ton programme : la performance (i) et/ou comportement (ii) et aussi leurs exigences de performance.

Dans le cas de comportements incorrects, je crois que le consensus est que le développeur doit trouver et corriger des copies ; déjà dans les cas où ces copies n’affectent pas la performance, je dirais que le développeur doit analyser les avantages/inconvénients avant d'investir le temps pour se retrouver/corriger ces copies. Cela étant dit, si vous utilisez Visual Studio vous pouvez installer le plugin ErrorProne.NET que facilitera beaucoup la localisationde ces problèmes (Jetbrains Rider a un plugin intégré pour faire la même chose).

Finalement, si vous êtes intéressé à mieux comprendre la plate-forme.Net , je vous recommande de suivre ce blog

Amusez-vous,

Adriano

No comments: