web-dev-qa-db-fra.com

Utilisation de vecteurs de pointeurs partagés vers des objets dans mon code C ++ pour éviter la duplication d'objets

Dans mon projet C++, j'ai trois classes, Particle, Contact et Network. La classe Network aura N particules (std::vector<Particle> particles) Et Nc contacts (std::vector<Contact> contacts). Les objets Particle auront chacun un certain nombre de contacts, représentés par des objets Contact, et les contacts sont partagés par des paires de particules.

Il sera nécessaire pour moi de savoir quels contacts une particule spécifique a, et j'ai décidé de pouvoir obtenir ces informations via l'objet Network (par exemple, en utilisant network.getContacts(Particle particle)).

Ma question peut être divisée en deux parties:

First, quelle serait une manière efficace et recommandée par la communauté de stocker les contacts pour chaque particule?

Je crois que je peux soit créer un vecteur de vecteur de pointeurs partagés vers Contact objets, vector<vector<shared_ptr<Contact>>> prtContacts (Où j'ai laissé std:: Pour faciliter la visualisation), de sorte que prtContacts[i] Contiendra un vecteur de pointeurs partagés vers les contacts de la ième particule. Il a été suggéré que l'utilisation de shared_ptr Dans ce contexte est utile, car cela garantira que je ne crée pas d'objets Contact en double pour deux particules avec un contact partagé. Existe-t-il une méthode alternative qui aurait plus de sens?

Second, si je finis par créer prtContacts comme défini ci-dessus, quelle est la meilleure façon de l'initialiser? Ma pensée actuelle est d'avoir une boucle initiale qui initialise std::vector<Contact> contacts Avec les contacts Nc, puis une deuxième boucle qui initialise prtContacts avec des pointeurs partagés vers les contacts. Est-ce raisonnable ou, encore une fois, existe-t-il des approches alternatives plus efficaces?

Je vous remercie!


Quelques informations supplémentaires:

Le nombre de particules, N, variera de O(10^1) à peut-être O(10^4). Le nombre de contacts par particule ne dépassera probablement pas 6-7, mais pourra varier. Chaque contact sera associé à une position et à une force de contact (vecteur à 2 composants), et les positions et les forces de contact pourront varier au cours des simulations (par exemple, les simulations de Monte Carlo).

9
AnInquiringMind

La bonne solution dépendra beaucoup de vos besoins non fonctionnels. Combien de particules aurez-vous généralement? Combien de contacts une particule a-t-elle généralement? Combien un réseau est-il mis à jour et de quelles manières? À quelle vitesse doit-il être?

Je suggère de faire référence aux objets par index si vous le pouvez, puis les contacts de particules peuvent être une liste d'index de contacts. Si une particule a généralement un petit nombre de contacts, au lieu d'un vector je suggère d'utiliser quelque chose qui est optimisé pour un petit nombre d'éléments comme boost::container::small_vector, absl::inlined_vector, folly::small_vector ou llvm::SmallVector.

using ParticleContacts = boost::container::small_vector<int, 12>;

class Network {
  std::vector<Particle> particles;
  std::vector<Contact> contacts;
  std::vector<ParticleContacts> particle_contacts;
 public:
  ParticleContacts getParticleContacts(int particle_index) const {
    return particle_contacts.at(particle_index);
  }
  Contact getContact(int contact_index) const {
    return contacts.at(contact_index);
  }
  //...
};

Démo en direct .

Edit: Si vous devez pouvoir supprimer du milieu du vector des contacts, alors utiliser un index ne fonctionnera pas. Je considérerais sérieusement si vous pouvez changer votre algorithme pour que ce ne soit pas nécessaire. Non seulement cela invalide les pointeurs, les itérateurs et les index, mais il est également assez inefficace car tous les contacts après la suppression doivent être déplacés. Par exemple, vous pouvez peut-être simplement laisser des contacts orphelins jusqu'à la fin de la simulation sans connexion à une particule. Quoi qu'il en soit, si vous avez vraiment besoin de pouvoir supprimer du milieu de la liste des contacts, un vector n'est peut-être pas la meilleure structure de données et vous devriez préférer quelque chose comme une liste chaînée. L'avantage d'une liste liée est que vous pouvez effectuer une suppression rapide à partir du milieu et que les pointeurs vers les contacts de la liste ne sont pas invalidés par des insertions ou des suppressions, de sorte que votre liste de contacts de particules peut être des pointeurs bruts. L'inconvénient est qu'il est beaucoup plus lent et ne permet pas un accès aléatoire.

using ParticleContacts = boost::container::small_vector<Contact*, 12>;

class Network {
  std::vector<Particle> particles;
  std::forward_list<Contact> contacts;
  std::vector<ParticleContacts> particle_contacts;
 public:
  ParticleContacts getParticleContacts(int particle_index) const {
    return particle_contacts.at(particle_index);
  } 
  void addParticle(const Particle& particle) {
    particles.Push_back(particle);
    particle_contacts.emplace_back();
  } 
  Contact* addParticleContact(int particle_index_1, int particle_index_2) {
    contacts.emplace_front();
    particle_contacts.at(particle_index_1).Push_back(&contacts.front());
    particle_contacts.at(particle_index_2).Push_back(&contacts.front());  
    return &contacts.front();
  }
};

Démo en direct .

Vous pouvez également utiliser std::vector<std::unique_ptr<Contact>>. Les pointeurs bruts vers le contact ne seront pas invalidés par les insertions et les suppressions. Il a l'avantage de permettre un accès aléatoire mais l'inconvénient d'être plus lent lors des insertions et suppressions: démo en direct

Le bon choix dépendra de la manière dont les données sont généralement accessibles par la simulation.

7
Chris Drew

Donc, cela dépend vraiment des modèles d'accès que vous prévoyez. Première, shared_ptr a un coût potentiellement élevé dans un programme multi-thread. L'incrémentation et la décrémentation du nombre de références entraîneront l'invalidation du cache du processeur sur les autres CPU. Cela entraînera un coût légèrement plus élevé pour ces opérations, et entraînera un important écrasement du cache si ces objets sont accessibles de quelque manière que ce soit à partir de plusieurs threads, car même l'accès en lecture aux octets proches du nombre de références entraînera une mémoire principale très coûteuse. récupérer après la mise à jour du décompte de références.

Maintenant, une façon d'atténuer cela est de ne pas utiliser make_shared comme on vous le dit. Il en résultera que le décompte de références sera alloué séparément de l'objet. Mais cela déplace potentiellement le problème de cache ailleurs.

Les différents types de pointeurs concernent l'expression de la propriété. L'objet vraiment a-t-il une propriété partagée? Ou est-il plus approprié de penser le Réseau dans son ensemble comme propriétaire de l'objet? S'il s'agit de ce dernier, il peut être plus judicieux de stocker des pointeurs nus et d'avoir simplement quelques vector objets (un par type) dans le réseau qui contiennent unique_ptrs à tous les éléments du graphique.

Le nombre de contacts par particule est-il limité à un nombre raisonnablement petit? Si tel est le cas, il peut être judicieux d'utiliser une taille fixe array qui stocke une liste de pointeurs nus vers les contacts et possède un objet Network qui possède réellement tous les objets.

Je suis sûr qu'il y a plus de considérations. Et mon objectif principal ici a été la performance plutôt qu'une notion abstraite de la "bonne" façon de faire les choses. Peut-être que quelque chose d'autre que la performance est votre objectif principal. Cependant, si c'est le cas, pourquoi utilisez-vous C++ ici?

3
Omnifarious

Je ferais largement écho à la réponse de Chris, mais avec quelques écarts possibles. L'ultime chose que je ferais sans réserve est de stocker des indices pour vos particules et contacts, stockés dans de grandes séquences comme un couple de grands réseaux pers vectoriels.

Cependant, une partie où je ne suis pas d'accord est que vous pouvez utiliser des indices et une séquence d'accès aléatoire et avoir toujours des suppressions à temps constant (avec une très petite constante) sans invalidation d'index si vous utilisez un indexé stratégie de liste/trous gratuite. Si des particules meurent, vous pouvez les supprimer en temps constant et récupérer/insérer en temps constant sans utiliser de mémoire supplémentaire, comme ceci:

enter image description here

... où union Nodes nodes[n] pourrait être std::vector<union Node>. Cela prend un peu plus d'huile de coude si Particle ou Contact n'est pas trivialement constructible ou destructible (faire un bon conteneur en C++ pour les non-PODs est un peu un PITA avec un placement nouveau, aligné stockage et invocations manuelles du dtor). Avec un peu de chance, comme c'est généralement le cas pour ce type de données.

Premièrement, quelle serait une façon efficace et recommandée par la communauté de stocker les contacts pour chaque particule?

En supposant qu'il soit efficace de simuler et de rassembler les contacts de toutes les particules à la fois (ce qui est souvent le cas dans de nombreuses simulations), réponse courte:

// Stores all the contacts between particles. Each per-particle 
// contact list begins with a count followed by the indices to each
// contact.
std::vector<int> contact_data;

// Stores an index into the contact data for each particle.
std::vector<int> contacts;

Pour obtenir les contacts de la nième particule, nous faisons:

// Fetch the index into the contact data for the nth particle.
const int cd_index = contacts[n];

// Fetch the number of contacts.
const int num = contact_data[cd_index];

// Fetch a pointer to the array of contact indices.
const int* indices = contact_data.data() + cd_index + 1;

Et maintenant vous avez les indices des contacts et leur nombre pour la nième particule. Vous pouvez effacer la liste et la calculer pour chaque pas de temps. Vous pouvez également le faire en multithread avec un peu de travail (rassembler les contacts pour les plages de particules dans chaque fil localement, puis fusionner les résultats dans les deux vecteurs à la fin).

network.getContacts(Particle particle)

Si vous suivez la suggestion d'utiliser des indices, ce qui précède se transforme en ceci:

// Returns the number of contacts and a pointer to the array
// of contact indices for the particle indexed by 'particle_index'.
std::pair<int, const int*> network.getContacts(int particle_index)

... vous pouvez utiliser quelque chose d'un peu plus agréable que std::pair, comme votre propre structure ou classe.

Petits vecteurs et similaires

Maintenant, utiliser quelque chose comme boost::small_vector Ou llvm::SmallVector Est une très bonne solution, bien supérieure à exiger une allocation de tas pour chaque particule comme ce serait le cas si vous utilisiez std::vector Pour chacune . La réponse de Chris est déjà très bonne. Mais si vous souhaitez stocker de manière persistante les contacts, ces petites optimisations de tampon commencent à devenir un peu explosives dans l'utilisation de la mémoire, ce qui peut se traduire par des échecs de cache supplémentaires (ils sont plutôt parfaits pour le stockage de courte durée, mais sur la pile), c'est pourquoi Je recommande juste un gros vieux vecteur d'entiers à la place avec l'hypothèse que votre simulation pourrait être mieux de calculer tous les contacts à la fois plutôt que de les calculer à la demande dans getContacts (à quel point un petit vecteur commencerait à devenir beaucoup plus approprié).

[...] dans ce contexte est utile, car il garantira que je ne crée pas d'objets Contact en double pour deux particules avec un contact partagé. Existe-t-il une méthode alternative qui aurait plus de sens?

shared_ptr Sert à partager la propriété d'une ressource et à prolonger sa durée de vie jusqu'à ce que tous les propriétaires libèrent leur propriété. Pour simplement faire référence à une ressource à plusieurs endroits sans partager la propriété (ce qui est tout ce dont vous avez besoin ici) et sans duplication des données de la ressource, utilisez simplement de vieux indices ou pointeurs. Il y a une surcharge relative assez lourde pour shared_ptr Utilisé sur une base par particule ou par contact.

Une chose rapide dans la pratique en utilisant les techniques ci-dessus (y compris la liste libre indexée), et pas vraiment des "particules" dans ce cas mais toujours des agents qui entrent en collision les uns avec les autres et une liste de collisions entre agents (regroupées en une seule fois en deux vecteurs d'entiers comme indiqué ci-dessus): 500 000 agents qui rebondissent les uns sur les autres, à un seul thread, avec une bonne partie du temps passé à tracer des pixels et pas seulement à simuler:

enter image description here

Premièrement, quelle serait une façon efficace et recommandée par la communauté de stocker les contacts pour chaque particule?

Si vous voulez faire les choses très efficacement lorsque la tentation est de stocker une cargaison de petites listes, stockez idéalement toutes les données dans de grandes séquences, pas un tas de minuscules. Vous n'avez en fait pas besoin d'instancier un million de conteneurs pour représenter l'équivalent d'un million de conteneurs associés à un million d'éléments. Vous pouvez simplement en utiliser deux, par exemple: un stockant toutes les données et un parallèle au million d'éléments stockant les indices de départ dans ces données.

Il s'agit d'une stratégie d'optimisation générale quel que soit le contexte, et elle aborde un problème de performance courant dans les langues qui fournissent de nombreux conteneurs pratiques. Ces conteneurs sont très efficaces pour stocker un million d'éléments dans un conteneur, mais pas très efficaces pour stocker un million de conteneurs avec quelques éléments chacun. Cela s'applique même à ceux qui utilisent SBO (petites optimisations de tampon) comme SmallVector ou std::string, Parce que le "petit tampon" est soit trop grand (à quel point nous gaspillons de la mémoire et obtenons d'énormes progrès d'un élément à l'autre) ou trop petites et entraînant des allocations de tas. Ce n'est généralement pas "juste ce qu'il faut" comme c'est le cas si vous utilisez un seul grand tampon pour toutes les données.

Notez que cette réponse peut être un peu exagérée. Je ne prends pas vraiment en compte la productivité et je suppose que vous avez un peu plus de temps pour faire face aux inconvénients de tout stocker dans un grand conteneur et éventuellement de mettre en œuvre cette liste gratuite indexée ci-dessus (je ne suis pas une bonne personne à demander des solutions efficaces qui peuvent être implémentées en 10 minutes ... eh bien, je pense que je peux les implémenter en 10 minutes mais seulement parce que j'ai eu beaucoup de pratique). Pour les effets visuels, il n'est pas rare de traiter des centaines de millions de particules, et je suis donc souvent obligé d'utiliser ces types de techniques et de conteneurs de handroll au strict minimum juste pour obtenir des performances acceptables sans que les utilisateurs ne se plaignent que certains autres logiciels est plus rapide ou que les studios passent à la simulation avec autre chose dans leurs fermes. Mais je suis un peu excité de partager des techniques très générales pour accélérer les choses, et une bonne qui ne devient pas trop sophistiquée et qui, espérons-le, n'est pas trop exagérée, consiste simplement à indexer vos éléments et à tout stocker dans de grands conteneurs, pas une cargaison de les petits. En fait, je me retiens beaucoup pour cette réponse car il y a beaucoup plus que je peux couvrir sur ce sujet, y compris une alternative à cette liste gratuite indexée pour les entrées à plus grande échelle (beaucoup plus complexe à mettre en œuvre), mais je m'arrête ici!

1
user204677

Je dois demander - pourquoi les Contacts sont-ils séparés des Particles? Il semble que chaque Particle devrait contenir une liste de ses contacts. Ou si ce n'est pas approprié, peut-être un autre class ou struct qui contient un Particle et un tableau (ou vector ou autre) de Contacts. Quelque chose comme:

struct ParticleCollision {
    Particle part;
    std::vector<Contact> contacts;
};

Le Network contiendrait alors un std::vector de ParticleCollisions.

0
user1118321