OutOfLine - modèle en mémoire pour les applications C ++ hautes performances

En travaillant chez Headlands Technologies, j'ai eu la chance d'écrire plusieurs utilitaires pour simplifier la création de code C ++ hautes performances. Cet article offre un aperçu général de l'un de ces utilitaires, OutOfLine .


Commençons par un exemple illustratif. Supposons que vous disposiez d'un système qui gère un grand nombre d'objets du système de fichiers. Il peut s'agir de fichiers ordinaires, appelés sockets ou tuyaux UNIX. Pour une raison quelconque, vous ouvrez de nombreux descripteurs de fichiers au démarrage, puis travaillez intensivement avec eux, et à la fin, fermez les descripteurs et supprimez les liens vers les fichiers (environ. La voie signifie la fonction de dissociation ).


La version initiale (simplifiée) peut ressembler à ceci:


 class UnlinkingFD { std::string path; public: int fd; UnlinkingFD(const std::string& p) : path(p) { fd = open(p.c_str(), O_RDWR, 0); } ~UnlinkingFD() { close(fd); unlink(path.c_str()); } UnlinkingFD(const UnlinkingFD&) = delete; }; 

Et c'est une bonne conception, logiquement saine. Il s'appuie sur RAII pour libérer automatiquement le descripteur et supprimer le lien. Vous pouvez créer un grand tableau de ces objets, travailler avec eux et lorsque le tableau cesse d'exister, les objets eux-mêmes effaceront tout ce qui était nécessaire dans le processus.


Mais qu'en est-il de la performance? Supposons que fd utilisé très souvent et path uniquement lors de la suppression d'un objet. Maintenant, le tableau se compose d'objets de taille 40 octets, mais souvent, seuls 4 octets sont utilisés. Cela signifie qu'il y aura plus de ratés dans le cache, car vous devez «ignorer» 90% des données.


L'une des solutions courantes à ce problème est la transition d'un tableau de structures à une structure de tableau. Cela fournira les performances souhaitées, mais au prix de l'abandon du RAII. Existe-t-il une option qui combine les avantages des deux approches?


Un compromis simple serait de remplacer std::string taille de 32 octets par std::unique_ptr<std::string> , dont la taille n'est que de 8 octets. Cela réduira la taille de notre objet de 40 octets à 16 octets, ce qui est une grande réussite. Mais cette solution perd toujours l'utilisation de plusieurs tableaux.


OutOfLine est un outil qui permet sans abandonner RAII de déplacer complètement les champs (froids) rarement utilisés à l'extérieur de l'objet. OutOfLine est utilisé comme classe de base CRTP , donc le premier argument du modèle doit être une classe enfant. Le deuxième argument est le type de données (froides) rarement utilisées qui est associé à un objet (principal) fréquemment utilisé.


 struct UnlinkingFD : private OutOfLine<UnlinkingFD, std::string> { int fd; UnlinkingFD(const std::string& p) : OutOfLine<UnlinkingFD, std::string>(p) { fd = open(p.c_str(), O_RDWR, 0); } ~UnlinkingFD(); UnlinkingFD(const UnlinkingFD&) = delete; }; 

Alors, à quoi ressemble cette classe?


 template <class FastData, class ColdData> class OutOfLine { 

L'idée de base de l'implémentation est d'utiliser un conteneur associatif global qui mappe les pointeurs sur les objets principaux et les pointeurs sur les objets qui contiennent des données froides.


  inline static std::map<OutOfLine const*, std::unique_ptr<ColdData>> global_map_; 

OutOfLine peut être utilisé avec tout type de données froides, dont une instance est créée et associée automatiquement à l'objet principal.


  template <class... TArgs> explicit OutOfLine(TArgs&&... args) { global_map_[this] = std::make_unique<ColdData>(std::forward<TArgs>(args)...); } 

La suppression de l'objet principal entraîne la suppression automatique de l'objet froid associé:


  ~OutOfLine() { global_map_.erase(this); } 

Lors du déplacement (constructeur de déplacement / opérateur d'affectation de déplacement) de l'objet principal, l'objet froid correspondant sera automatiquement associé au nouvel objet successeur principal. Par conséquent, vous ne devez pas accéder aux données froides d'un objet déplacé.


  explicit OutOfLine(OutOfLine&& other) { *this = other; } OutOfLine& operator=(OutOfLine&& other) { global_map_[this] = std::move(global_map_[&other]); return *this; } 

Dans l'exemple d'implémentation ci - dessus , OutOfLine est rendu non copiable pour plus de simplicité. Si nécessaire, les opérations de copie sont faciles à ajouter; il suffit de créer et de lier une copie d'un objet froid.


 OutOfLine(OutOfLine const&) = delete; OutOfLine& operator=(OutOfLine const&) = delete; 

Maintenant, pour que cela soit vraiment utile, ce serait bien d'avoir accès à des données froides. Lorsqu'elle hérite d' OutOfLine classe reçoit les méthodes constantes et non constantes de cold() :


  ColdData& cold() noexcept { return *global_map_[this]; } ColdData const& cold() const noexcept { return *global_map_[this]; } 

Ils renvoient le type de référence approprié aux données froides.


C’est presque tout. Cette option UnlinkingFD aura une taille de 4 octets, fournira un accès compatible avec le cache au champ fd et conservera les avantages de RAII. Tous les travaux liés au cycle de vie d'un objet sont entièrement automatisés. Lorsque l'objet principal fréquemment utilisé se déplace, les données froides rarement utilisées se déplacent avec lui. Lorsque l'objet principal est supprimé, l'objet froid correspondant est également supprimé.


Parfois, cependant, vos données sont compliquées pour compliquer votre vie - et vous êtes confronté à une situation dans laquelle des données de base doivent être créées en premier. Par exemple, ils sont nécessaires pour construire des données froides. Il est nécessaire de créer des objets dans l'ordre inverse par rapport à ce OutOfLine propose OutOfLine . Dans de tels cas, une «sauvegarde» nous est utile pour contrôler l'ordre d'initialisation et de désinitialisation.


  struct TwoPhaseInit {}; OutOfLine(TwoPhaseInit){} template <class... TArgs> void init_cold_data(TArgs&&... args) { global_map_.find(this)->second = std::make_unique<ColdData>(std::forward<TArgs>(args)...); } void release_cold_data() { global_map_[this].reset(); } 

Il s'agit d'un autre constructeur OutOfLine qui peut être utilisé dans les classes enfants; il accepte une balise de type TwoPhaseInit . Si vous créez OutOfLine de cette manière, les données froides ne seront pas initialisées et l'objet restera à moitié construit. Pour terminer la construction en deux phases, vous devez appeler la méthode init_cold_data (en lui passant les arguments nécessaires pour créer un objet de type ColdData ). N'oubliez pas que vous ne pouvez pas appeler .cold() sur un objet dont les données froides n'ont pas encore été initialisées. Par analogie, les données froides peuvent être supprimées avant la date prévue avant d'exécuter le destructeur ~OutOfLine en appelant release_cold_data .


 }; // end of class OutOfLine 

Maintenant c'est tout. Que nous apportent donc ces 29 lignes de code? Ils constituent un autre compromis possible entre performances et facilité d'utilisation. Dans les cas où vous avez un objet, dont certains membres sont utilisés beaucoup plus souvent que d'autres, OutOfLine peut servir de moyen facile à utiliser pour optimiser le cache, au prix d'un ralentissement significatif de l'accès aux données rarement utilisées.


Nous avons pu appliquer cette technique à plusieurs endroits - assez souvent, il est nécessaire de compléter les données de travail intensivement utilisées par des métadonnées supplémentaires nécessaires à la fin du travail, dans des situations rares ou inattendues. Qu'il s'agisse d'informations sur les utilisateurs qui ont établi la connexion, du terminal de négociation d'où provient l'ordre ou de la poignée de l'accélérateur matériel engagé dans le traitement des données d'échange - OutOfLine gardera le cache propre lorsque vous êtes dans la partie critique des calculs (chemin critique).


J'ai préparé un test pour que vous puissiez voir et évaluer la différence.


Le scriptTemps (ns)
Données froides dans l'objet principal (version initiale)34684547
Données froides complètement supprimées (meilleur scénario)2938327
Utilisation d'OutOfLine2947645

J'ai obtenu une accélération d'environ OutOfLine lors de l'utilisation d' OutOfLine . De toute évidence, ce test est conçu pour démontrer le potentiel d' OutOfLine , mais il montre également dans quelle mesure l'optimisation du cache peut avoir un impact significatif sur les performances, tout comme OutOfLine permet d'obtenir cette optimisation. Garder le cache exempt de données rarement utilisées peut apporter des améliorations complexes, mesurables et complètes au reste du code. Comme toujours avec l'optimisation, faites confiance aux mesures plus qu'aux hypothèses, néanmoins j'espère que OutOfLine se révélera être un outil utile dans votre collection d'utilitaires.


Note du traducteur


Le code fourni dans l'article sert à démontrer l'idée et n'est pas représentatif du code de production.

Source: https://habr.com/ru/post/fr421475/


All Articles