Comment trouver les opérations de copie parasite C ++?

11

Récemment, j'ai eu ce qui suit

struct data {
  std::vector<int> V;
};

data get_vector(int n)
{
  std::vector<int> V(n,0);
  return {V};
}

Le problème avec ce code est que lorsque la structure est créée, une copie se produit et la solution consiste à écrire return {std :: move (V)}

Existe-t-il un analyseur de linter ou de code qui détecterait de telles opérations de copie parasite? Ni cppcheck, cpplint, ni clang-tidy ne peuvent le faire.

EDIT: Plusieurs points pour clarifier ma question:

  1. Je sais qu'une opération de copie s'est produite car j'ai utilisé l' explorateur de compilateur et cela montre un appel à memcpy .
  2. Je pourrais identifier qu'une opération de copie s'est produite en regardant la norme oui. Mais ma mauvaise idée initiale était que le compilateur optimiserait cette copie. J'avais tort.
  3. Ce n'est (probablement) pas un problème de compilation car clang et gcc produisent du code qui produit un memcpy .
  4. Le memcpy peut être bon marché, mais je ne peux pas imaginer des circonstances où copier la mémoire et supprimer l'original est moins cher que de passer un pointeur par un std :: move .
  5. L'ajout de std :: move est une opération élémentaire. J'imagine qu'un analyseur de code pourrait suggérer cette correction.
Mathieu Dutour Sikiric
la source
2
Je ne peux pas dire s'il existe ou non une méthode / un outil pour détecter les opérations de copie "fallacieuses", cependant, à mon avis honnête, je ne suis pas d'accord que la copie du std::vectorpar quelque moyen que ce soit n'est pas ce qu'elle prétend être . Votre exemple montre une copie explicite, et il est naturel, et la bonne approche, (encore une fois à mon humble avis) d'appliquer la std::movefonction comme vous le suggérez si une copie n'est pas ce que vous voulez. Notez que certains compilateurs peuvent omettre la copie si les indicateurs d'optimisation sont activés et que le vecteur est inchangé.
Magnus
Je crains qu'il y ait trop de copies inutiles (qui pourraient ne pas avoir d'impact) pour rendre cette règle de linter utilisable: - / ( rouille utilise le déplacement par défaut, donc nécessite une copie explicite :))
Jarod42
Mes suggestions pour optimiser le code est essentiellement de démonter la fonction que vous souhaitez optimiser et vous découvrirez les opérations de copie supplémentaires
camp0
Si je comprends bien votre problème, vous souhaitez détecter les cas où une opération de copie (constructeur ou opérateur d'affectation) est invoquée sur un objet suivi de sa destruction. Pour les classes personnalisées, je peux imaginer ajouter un ensemble d'indicateurs de débogage lorsqu'une copie est effectuée, réinitialiser dans toutes les autres opérations et archiver le destructeur. Cependant, je ne sais pas comment faire de même pour les classes non personnalisées à moins que vous ne puissiez modifier leur code source.
Daniel Langr
2
La technique que j'utilise pour trouver des copies parasites consiste à rendre temporairement le constructeur de copie privé, puis à examiner où le compilateur rechigne en raison de restrictions d'accès. (Le même objectif peut être atteint en étiquetant le constructeur de copie comme obsolète, pour les compilateurs qui prennent en charge un tel étiquetage.)
Eljay

Réponses:

2

Je crois que vous avez la bonne observation mais la mauvaise interprétation!

La copie ne se produira pas en renvoyant la valeur, car chaque compilateur intelligent normal utilisera (N) RVO dans ce cas. Depuis C ++ 17, cela est obligatoire, vous ne pouvez donc pas voir de copie en renvoyant un vecteur généré localement à partir de la fonction.

OK, permet de jouer un peu avec std::vectoret ce qui se passera pendant la construction ou en le remplissant étape par étape.

Tout d'abord, permet de générer un type de données qui rend chaque copie ou déplacement visible comme celui-ci:

template <typename DATA >
struct VisibleCopy
{
    private:
        DATA data;

    public:
        VisibleCopy( const DATA& data_ ): data{ data_ }
        {
            std::cout << "Construct " << data << std::endl;
        }

        VisibleCopy( const VisibleCopy& other ): data{ other.data }
        {
            std::cout << "Copy " << data << std::endl;
        }

        VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
        {
            std::cout << "Move " << data << std::endl;
        }

        VisibleCopy& operator=( const VisibleCopy& other )
        {
            data = other.data;
            std::cout << "copy assign " << data << std::endl;
        }

        VisibleCopy& operator=( VisibleCopy&& other ) noexcept
        {
            data = std::move( other.data );
            std::cout << "move assign " << data << std::endl;
        }

        DATA Get() const { return data; }

};

Et maintenant, commençons quelques expériences:

using T = std::vector< VisibleCopy<int> >;

T Get1() 
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
    std::cout << "End init" << std::endl;
    return vec;
}   

T Get2()
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec(4,0);
    std::cout << "End init" << std::endl;
    return vec;
}

T Get3()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

T Get4()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.reserve(4);
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

int main()
{
    auto vec1 = Get1();
    auto vec2 = Get2();
    auto vec3 = Get3();
    auto vec4 = Get4();

    // All data as expected? Lets check:
    for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}

Que pouvons-nous observer:

Exemple 1) Nous créons un vecteur à partir d'une liste d'initialisation et nous nous attendons peut-être à voir 4 fois la construction et 4 mouvements. Mais nous en avons 4 exemplaires! Cela semble un peu mystérieux, mais la raison en est la mise en œuvre de la liste d'initialisation! Simplement, il n'est pas autorisé de se déplacer de la liste car l'itérateur de la liste est un const T*qui rend impossible le déplacement d'éléments de celle-ci. Une réponse détaillée sur ce sujet peut être trouvée ici: initializer_list et move semantics

Exemple 2) Dans ce cas, nous obtenons une construction initiale et 4 copies de la valeur. Ce n'est rien de spécial et c'est ce à quoi nous pouvons nous attendre.

Exemple 3) Ici aussi, nous avons la construction et certains mouvements comme prévu. Avec mon implémentation stl, le vecteur croît de facteur 2 à chaque fois. Nous voyons donc une première construction, une autre et parce que le vecteur se redimensionne de 1 à 2, nous voyons le déplacement du premier élément. En ajoutant le 3, nous voyons un redimensionnement de 2 à 4 qui nécessite un déplacement des deux premiers éléments. Tout comme prévu!

Exemple 4) Maintenant, nous réservons de l'espace et remplissons plus tard. Maintenant, nous n'avons plus de copie ni de mouvement!

Dans tous les cas, nous ne voyons aucun mouvement ni copie en renvoyant le vecteur à l'appelant! (N) RVO est en cours et aucune autre action n'est requise à cette étape!

Retour à votre question:

"Comment trouver des opérations de copie parasite C ++"

Comme vu ci-dessus, vous pouvez introduire une classe proxy entre les deux à des fins de débogage.

Rendre le copy-ctor privé peut ne pas fonctionner dans de nombreux cas, car vous pouvez avoir des copies désirées et des cachées. Comme ci-dessus, seul le code de l'exemple 4 fonctionnera avec un copieur privé! Et je ne peux pas répondre à la question, si l'exemple 4 est le plus rapide, car nous remplissons la paix par la paix.

Désolé de ne pas pouvoir proposer de solution générale pour trouver des copies "indésirables" ici. Même si vous creusez votre code pour les appels de memcpy, vous ne trouverez pas tout car il memcpysera également optimisé et vous verrez directement quelques instructions d'assembleur faire le travail sans appeler votre memcpyfonction de bibliothèque .

Mon indice n'est pas de se concentrer sur un problème aussi mineur. Si vous avez de vrais problèmes de performances, prenez un profileur et mesurez. Il y a tellement de tueurs potentiels de performances, qu'investir beaucoup de temps sur une memcpyutilisation intempestive ne semble pas une idée aussi valable.

Klaus
la source
Ma question est plutôt académique. Oui, il existe de nombreuses façons d'avoir du code lent et ce n'est pas un problème immédiat pour moi. Cependant, nous pouvons trouver les opérations memcpy en utilisant l'explorateur du compilateur. Il y a donc certainement un moyen. Mais cela n'est possible que pour les petits programmes. Mon point est qu'il y a un intérêt pour le code qui trouverait des suggestions sur la façon d'améliorer le code. Il existe des analyseurs de code qui détectent les bogues et les fuites de mémoire, pourquoi pas de tels problèmes?
Mathieu Dutour Sikiric
"code qui trouverait des suggestions sur la façon d'améliorer le code." Cela est déjà fait et implémenté dans les compilateurs eux-mêmes. L'optimisation (N) RVO n'est qu'un exemple unique et fonctionne parfaitement comme indiqué ci-dessus. La capture de memcpy n'a pas aidé car vous recherchez "memcpy indésirable". "Il existe des analyseurs de code qui détectent les bogues et les fuites de mémoire, pourquoi pas de tels problèmes?" Ce n'est peut-être pas un problème (courant). Et un outil beaucoup plus général pour trouver les problèmes de "vitesse" est également déjà présent: profiler! Mon sentiment personnel est que vous recherchez une chose académique qui n'est pas un problème dans les vrais logiciels d'aujourd'hui.
Klaus
1

Je sais qu'une opération de copie s'est produite car j'ai utilisé l'explorateur de compilateur et cela montre un appel à memcpy.

Avez-vous mis votre application complète dans l'explorateur du compilateur et avez-vous activé les optimisations? Sinon, ce que vous avez vu dans l'explorateur du compilateur pourrait ou non être ce qui se passe avec votre application.

Un problème avec le code que vous avez publié est que vous créez d'abord un std::vector, puis le copiez dans une instance de data. Il vaudrait mieux initialiser data avec le vecteur:

data get_vector(int n)
{
  return {std::vector<int> V(n,0)};
}

De plus, si vous donnez simplement à l'explorateur du compilateur la définition de dataet get_vector(), et rien d'autre, il doit s'attendre au pire. Si vous lui donnez réellement du code source qui utilise get_vector() , alors regardez quel assembly est généré pour ce code source. Consultez cet exemple pour savoir ce que la modification ci-dessus, l'utilisation réelle et les optimisations du compilateur peuvent entraîner la production du compilateur.

G. Sliepen
la source
Je n'ai mis dans l'explorateur informatique que le code ci-dessus (qui a la memcpy ) sinon la question n'aurait pas de sens. Cela étant dit, votre réponse est excellente en montrant différentes façons de produire un meilleur code. Vous disposez de deux méthodes: utilisation de statique et insertion directe du constructeur dans la sortie. Ainsi, ces façons pourraient être suggérées par un analyseur de code.
Mathieu Dutour Sikiric