2017-08-11 9 views
6

En C#, il existe des structures et des classes. Les structs sont généralement alloués (c'est-à-dire qu'il y a des exceptions) et les classes sont toujours allouées par tas. Les instances de classe exercent donc une pression sur le GC et sont considérées comme "plus lentes" que les structures. Microsoft a a best practice guide quand utiliser des structures sur les classes. Cela dit envisager une struct si:Existe-t-il une meilleure pratique lorsqu'un type doit être encadré?

  • Il représente logiquement une valeur unique, semblable à des types primitifs (int, double, etc.).
  • Il a une taille d'instance inférieure à 16 octets.
  • Il est immuable.
  • Il n'aura pas à être encadré fréquemment.

En C#, en utilisant des instances struct qui sont supérieurs à 16 octets est dit en général pour effectuer pire que les instances de classe les déchets collectés (allouée dynamiquement).

Quand est-ce qu'une instance encadrée (qui est allouée par tas) fonctionne mieux, en termes de vitesse, qu'une instance équivalente non-encadrée (qui est allouée par pile)? Existe-t-il une meilleure pratique sur quand nous devrions allouer dynamiquement (sur le tas) au lieu de coller à l'allocation de pile par défaut? TL; DR: commence sans boxe, puis profil.

Répondre

6

TL;


Allocation Stack vs Allocation Boxed

Ceci est peut-être plus nette:

  • bâton à la pile,
  • À moins que la valeur est assez grand qu'il soufflerait vers le haut.

Alors que sémantiquement écriture fn foo() -> Bar implique le transfert Bar du cadre callee au cadre de l'appelant, en pratique, vous êtes plus susceptibles de se retrouver avec l'équivalent d'une signature fn foo(__result: mut * Bar) où l'appelant alloue de l'espace sur sa pile et passe un pointeur vers l'appelé.

Cela peut ne pas toujours être suffisante pour éviter la copie, car certains modèles peuvent empêcher d'écrire directement dans la fente de retour:

fn defeat_copy_elision() -> WithDrop { 
    let one = side_effectful(); 
    if side_effectful_too() { 
     one 
    } else { 
     side_effects_hurt() 
    } 
} 

Ici, il n'y a pas de magie:

  • si l'utilisation du compilateur l'emplacement de retour pour one, puis dans le cas où la branche évalue à false il doit se déplacer one puis instancier le nouveau WithDrop dedans, et enfin détruire one,
  • Si le compilateur instancie one sur la pile en cours et qu'il doit le renvoyer, il doit effectuer une copie.

Si le type n'avait pas besoin de Drop, il n'y aurait pas de problème.

Malgré ces cas bizarres, je conseille de coller à la pile si possible, sauf si le profilage révèle un endroit où il serait avantageux de mettre en boîte.


Inline membre ou Boxed Membre

Cette affaire est beaucoup plus compliquée:

  • la taille du struct/enum est affecté, ainsi le comportement du cache du processeur est affectée:

    • moins fr les grandes variantes utilisées régulièrement sont un bon candidat pour la boxe (ou les parties de boxe d'entre eux),
    • les membres les moins fréquemment accédés sont un bon candidat pour la boxe.
  • en même temps, il y a des coûts pour la boxe:

    • il est incompatible avec Copy types, et met en œuvre implicitement Drop (qui, comme on le voit ci-dessus, désactive certaines optimisations),
    • allouant/freeing la mémoire a un temps de latence illimité ,
    • L'accès à la mémoire boîte introduit une dépendance aux données: vous ne pouvez pas savoir quelle ligne de cache demander avant de connaître l'adresse.

En conséquence, c'est un équilibre très fin. Boxe ou unboxing un membre peut améliorer les performances de certaines parties de la base de code tout en diminuant les performances des autres.

Il n'y a définitivement pas de taille unique.

Ainsi, encore une fois, je conseille d'éviter la boxe jusqu'à ce que le profilage révèle un endroit où il serait bénéfique de boxer.

Tenir compte que sur Linux, toute allocation de mémoire pour laquelle il n'y a pas de mémoire de rechange dans le processus peut nécessiter un appel système qui, s'il n'y a pas de mémoire de rechange dans le système d'exploitation peut déclencher le tueur OOM tuer un processus, à quel point sa mémoire est récupérée et rendue disponible. Un simple malloc(1) peut facilement exiger millisecondes.