6
votes

Devrais-je utiliser un alloc manuel pour permettre à déplacer la sémantique?

Je suis intéressé à apprendre lorsque je devrais commencer à envisager d'utiliser la sémantique de déplacement en faveur de la copie de données en fonction de la taille de ces données et de l'utilisation de la classe. Par exemple, pour une classe matrix4, nous avons deux options:

struct Matrix4{
    float* data;

    Matrix4(){ data = new float[16]; }
    Matrix4(Matrix4&& other){
        *this = std::move(other);
    }
    Matrix4& operator=(Matrix4&& other)
    {
       ... removed for brevity ...
    }
    ~Matrix4(){ delete [] data; }

    ... other operators and class methods ...
};

struct Matrix4{
    float data[16]; // let the compiler do the magic

    Matrix4(){}
    Matrix4(const Matrix4& other){
        std::copy(other.data, other.data+16, data);
    }
    Matrix4& operator=(const Matrix4& other)
    {
       std::copy(other.data, other.data+16, data);
    }

    ... other operators and class methods ...
};


3 commentaires

Impossible à estimer sans la mesure exacte de la mesure dans des scénarios spécifiques. BTW, dans le deuxième cas && opérateur n'est pas requis, car cela fonctionne exactement comme exploitant. La première version peut être préférable, si vous effectuez une classe mondiale des modèles.


C'est une faute de frappe, je me coupe a collé le code ci-dessus. Merci de l'avoir tapé.


faux dilemme : vous Devrait Fournir une sémantique de déplacement quand c'est logique. Vous devriez pas Utiliser la gestion de la mémoire manuelle.


3 Réponses :


-1
votes

J'utiliserais probablement un conteneur STDLIB (tel que STD :: Vector ou STD :: Array) qui implémente déjà la sémantique de déplacer, puis je voudrais simplement déménager les vecteurs ou les tableaux.

Par exemple, vous pouvez utiliser STD :: Array ou STD :: Vecteur > pour représenter votre type de matrice.

Je ne pense pas que cela comptera beaucoup pour une matrice 4x4, mais cela pourrait pour 10000x10000. Donc, oui, un constructeur de déplacement pour un type de matrice en vaut vraiment la peine , surtout si vous envisagez de travailler avec de nombreuses matrices temporaires (ce qui semble probablement lorsque vous souhaitez effectuer des calculs avec eux). Il permettra également de retourner efficacement les objets Matrix4 (alors que vous devrez utiliser un appel par référence pour contourner la copie autrement).

Plutôt sans rapport avec la matière mais il convient de mentionner probablement: au cas où vous décideriez d'utiliser STD :: Array, veuillez faire une matrice une classe de modèle (au lieu d'intégrer la taille dans le nom de classe).


7 commentaires

Cela ne répondrait-il vraiment à la question cependant, qui devient alors "std :: vecteur? (Option 1) ou std :: Array? (Option 2)". L'utilisation de ces conteneurs simplifie certainement le code dans les deux cas.


@juanchopanza à la fois STD :: Array et STD :: Vecteur Destructeurs de déplacement. Je recommanderais STD :: Array dans cette situation cependant. Réponse courte: "Donc oui, un constructeur de déplacement pour un type de matrice en vaut vraiment la peine"


Mais un mouvement pour un std :: tableau est juste une copie. STD :: Array n'a pas de poignée pratique aux données pour permettre un geste efficace. Il est les données.


@juanchopanza non, je ne pense pas que ce soit. Si vous déplacez un std :: Array Tout ce que ses éléments seront déplacés (ce qui n'est pas une copie). Aussi auto correction: "Autoriser le déplacement de la sémantique" (pas "avoir des constructeurs de déplacement")


Même si c'était le cas, vous devriez toujours créer un tableau avec n éléments, puis déplacer chacun des N du RHS dans le LHS. Ceci est O (n), et dans tous les cas ne serait un avantage que pour les types avec des mouvements efficaces et float n'est pas l'un de ceux-ci.


@Anothertest: A std :: vecteur ne déplace pas ses éléments, il déplace simplement le pointeur sur les éléments (bon marché). Un std :: Array D'autre part, déplace tous ses éléments et, étant donné que, dans ce cas, les éléments ne disposent pas d'un constructeur de déplacement, ils seront copiés (chers) ...


@ronag en effet, c'est absolument correct. Mon mauvais, aurait dû voir qu'il utilise des flotteurs ... ce qui signifie qu'il pourrait utiliser std :: vecteur à la place.



9
votes

Dans le premier cas, l'allocation et la répartition sont coûteuses - car vous allociez de manière dynamique la mémoire du tas, même si votre matrice est construite sur la pile - et les mouvements sont bon marché (copier simplement un pointeur).

Dans le second cas, l'allocation et la répartition sont bon marché, mais les mouvements sont chers - car ils sont en réalité des copies.

Donc, si vous écrivez une application et que vous vous souciez de la performance de que application, la réponse à la question " la meilleure? "probablement dépend du montant que vous créez / détruisant des matrices vs à quel point vous copiez / déplacez-les - et dans tous les cas, Faites vos propres mesures pour supporter les conjectures.

En effectuant des mesures, vous vérifierez également si votre compilateur fait beaucoup de copies / déplacez des éliminations dans des endroits où vous vous attendez à ce que les résultats puissent être utilisés - les résultats peuvent être contre vos attentes.

En outre, la localité de cache peut avoir un impact ici: Si vous allouez le stockage des données de la matrice sur le tas, trois matrices que vous souhaitez traiter Élément par élément créé sur la pile nécessiteront probablement un accès à la mémoire assez dispersé. motif - entraîner potentiellement une perte de cache.

D'autre part, si vous utilisez des tableaux pour lesquels la mémoire est allouée sur la pile, il est probable que la même ligne de cache soit en mesure de contenir les données de toutes ces matrices - augmentant ainsi le taux de frappe de cache. Sans oublier le fait que pour accéder aux éléments sur le tas, vous devez d'abord lire la valeur du pointeur Data , ce qui signifie accéder à une région de mémoire différente de celle qui détient les éléments.

Donc, une fois de plus, la morale de l'histoire est la suivante: Faites vos propres mesures .

Si vous écrivez une bibliothèque d'autre part, et que vous ne pouvez pas prédire le nombre de constructions / destructions vs déplacées / copies au client, vous pouvez offrir deux < / em> ces classes matricielles et facticent le comportement commun dans une classe de base - éventuellement une classe de base modèle .

Cela donnera à la flexibilité du client et vous donnera un degré de réutilisation suffisamment élevé - pas besoin d'écrire la mise en œuvre de toutes les fonctions de membre communes deux fois.

De cette façon, les clients peuvent choisir la classe matricielle qui correspond le mieux au profil de création / déplacement de l'application dans laquelle ils l'utilisent.


mise à jour:

comme Dimalmg souligne dans les commentaires, un avantage de l'approche de la matrice sur l'approche d'allocation dynamique est que Ce dernier effectue une gestion manuelle des ressources via des pointeurs bruts, nouveau et Supprimer , qui vous oblige à écrire destructeurs définis par l'utilisateur, constructeur de copie, moteur de déplacement, opérateur d'affectation de copie et opérateur d'assignation de déplacement.

Vous pouvez éviter tout cela si vous utilisiez std :: vecteur , qui effectuerait la tâche de gestion de la mémoire pour vous et vous éviterait du fardeau de la définition de toutes ces fonctions spéciales. < / p>

Cela dit, le simple fait de suggérer d'utiliser std :: vecteur au lieu de la gestion manuelle de la mémoire - autant qu'il est un bon conseil en termes de pratique de conception et de programmation - ne répond pas la question, alors que je crois que la réponse originale fait.


7 commentaires

Oui, mon cas consiste à créer une bibliothèque, donc j'avais également envisagé de mesurer à la fois des options et de le documenter. Merci pour les conseils.


@AndyProwl: Je pense qu'un autre facteur très important de décider d'une implémentation est que, lors de la mise en place des données en dehors de l'instance, vous pourriez générer un Cache Mlle chaque fois que vous y accédez une partie. Ceci n'est évidemment que significatif pour les petites classes. Dans l'ensemble, bien sûr, vous avez raison de recommander que l'utilisateur doit mesurer dans son domaine d'application avec ses habitudes d'utilisation et ses données et décider elle-même, mais je pense (comme je décris dans ma réponse), mettez les données à l'intérieur de la classe. la valeur par défaut à moins qu'il y ait une raison de ne pas le faire.


@YZT: C'est vrai. J'y ai pensé mais j'ai décidé de le laisser hors de la réponse parce que cela me semblait avoir la part de "faire ta propre mesure" la subsua. Réfléchir à deux reprises, je suppose que cela pourrait valoir la peine de mentionner. Permettez-moi de l'ajouter à la réponse. Merci


Oui, cela est en fait assez important et je l'ai manqué lorsque vous envisagez les deux implémentations.


Toutes les réponses sont intéressantes, mais celle-ci a été la première à résumer tous les compromis et pointe le lecteur à mesurer en tant que bon outil pour ce problème. Je la marquise comme le bon.


Vous manquez ici du point le plus fondamental, la situation "manuelle manuelle" de l'OP est non-Raii qui est terrible. S'il devait utiliser un std :: vecteur, ce serait beaucoup plus comparable. Il ne peut pas faire l'approche d'alloc dynamique en toute sécurité s'il ne peut pas être raii.


@Deadmg: Alors que je suis d'accord sur le fait que l'utilisation de std :: vecteur est une meilleure pratique de programmation, pourquoi pensez-vous que c'est " le point le plus fondamental "? Afaict, l'OP demande une comparaison de performance entre une allocation dynamique de la matrice (quel std :: vecteur ferait) vs un membre de données de tableau. Oui, en utilisant std :: Array <> et std :: vecteur <> est une meilleure idée, mais la question devient " devrais-je utiliser std :: Array ou std :: vecteur ". C'est ce que je voulais répondre.



2
votes

Comme tout le reste de la programmation, spécialement, lorsque la performance est concernée, c'est un compromis compliqué.

Ici, vous avez deux conceptions: pour conserver les données à l'intérieur de votre classe (méthode 1) ou pour allouer les données sur le tas et conserver un pointeur dans la classe (méthode 2).

Autant que je puisse dire, ce sont les compromis que vous faites:

  1. Vitesse de construction / destruction : naïvement implémentée, la méthode 2 sera plus lente ici, car elle nécessite une allocation de mémoire dynamique et une répartition. Cependant, vous pouvez aider la situation à utiliser des allocateurs de mémoire personnalisés, spécialement si la taille de vos données est prévisible et / ou fixe.

  2. taille : Dans votre exemple matriciel 4x4, la méthode 2 nécessite de stocker un pointeur supplémentaire, plus la taille de la taille de la taille de la mémoire (généralement peut-être entre 4 et 32 ​​octets.) Cela pourrait ou pourrait ne pas être un facteur, mais il faut certainement être pris en compte, spécialement si vos instances de classe sont petites.

  3. vitesse de déplacement : la méthode 2 est très rapide déplacer opération, car il ne nécessite que de définir deux pointeurs. Dans la méthode 1, vous n'avez d'autre choix que de copier vos données. Cependant, tout en pouvant s'appuyer sur le déplacement rapide, vous pouvez rendre votre code jolie et simple et lisible et plus efficace, les compilateurs sont assez bons à copier élision , ce qui signifie que vous pouvez écrire votre jolie, simple et lisible. interfaces pass-valeur, même si vous implémentez la méthode 1 et que le compilateur ne générera pas trop de copies de toute façon. Mais vous ne pouvez pas en être sûr, si vous compilez sur cette optimisation de compilateur, spécialement si vos instances sont plus grandes, nécessitent une mesure et une inspection du code généré.

  4. Vitesse d'accès membre : C'est le la différenciation la plus importante pour les petites classes , à mon avis. Chaque fois que vous accédez à un élément dans une matrice implémentée à l'aide de la méthode 2 (ou accédez à un champ d'une classe implémentée de cette façon, c'est-à-dire avec des données externes), vous accédez à la mémoire deux fois: une fois pour lire l'adresse du bloc de mémoire externe et une fois pour lire les données souhaitées. Dans la méthode 1, vous accédez juste directement au champ ou à l'élément souhaité. Cela signifie que dans la méthode 2, chaque accès pourrait potentiellement générer une erreur supplémentaire , ce qui pourrait affecter votre performance. Ceci est particulièrement important si vos instances de classe sont petites (par exemple une matrice 4x4) et que vous opérez sur plusieurs d'entre eux stockés dans des tableaux ou des vecteurs.

    En fait, c'est pourquoi vous voudrez peut-être réellement copier des octets lorsque vous copiez / déplaçant une instance de votre matrice, au lieu de simplement définir un pointeur: pour garder vos données contiguës . C'est pourquoi les structures de données plates (les matières de valeurs telles que les valeurs) sont beaucoup préférées dans le code hautes performances que les structures de données du pointeur Spaghetti (comme des tableaux de pointeurs, des listes liées, etc.), alors que le déplacement est plus froid et plus rapide que la copie < em> isolément , vous voulez parfois faire vos instances pour faire (ou garder) tout un tas d'entre eux contigu et faire une itération et leur accéder beaucoup plus efficacement.

  5. flexibilité de longueur / taille : la méthode 2 est évidemment plus flexible à cet égard car vous pouvez décider de la quantité de données dont vous avez besoin au moment de l'exécution, que ce soit 16 ou 16777216 octets.

    Dans l'ensemble, voici l'algorithme que je vous suggère d'utiliser pour choisir une implémentation:

    • Si vous avez besoin de quantité variable de données, sélectionnez la méthode 2.
    • Si vous avez de très grandes quantités de données dans chaque instance de votre classe (par exemple plusieurs kilo-octets,) Méthode de sélection 2.
    • Si vous devez copier des instances de votre classe autour beaucoup (et que je veux dire beaucoup!) Choisissez la méthode 2 (mais essayez de mesurer l'amélioration de la performance et d'inspecter le code généré, spécialement dans les zones chaudes.)
    • Dans tous les autres cas, préférez la méthode 1.

      En bref, la méthode 1 devrait être votre valeur par défaut, jusqu'à preuve contraire. Et la façon de prouver quelque chose en ce qui concerne la performance est mesure ! Donc, n'opinez rien sauf si vous avez mesuré et que vous avez la preuve qu'une méthode est meilleure qu'une autre, et également (comme indiqué dans d'autres réponses), vous souhaiterez peut-être mettre en œuvre les deux méthodes si vous écrivez une bibliothèque et laissez vos utilisateurs choisir le Mise en œuvre.


0 commentaires