Come trovare operazioni di copia spuria in C ++?


11

Di recente, ho avuto il seguente

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

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

Il problema con questo codice è che quando viene creata la struttura si verifica una copia e la soluzione è invece scrivere return {std :: move (V)}

Esistono analizzatori di codici o di codici che potrebbero rilevare tali operazioni di copia spuria? Né cppcheck, cpplint, né clang-ordinato possono farlo.

EDIT: diversi punti per chiarire la mia domanda:

  1. So che si è verificata un'operazione di copia perché ho utilizzato Explorer del compilatore e mostra una chiamata a memcpy .
  2. Potrei identificare che si sono verificate operazioni di copia osservando lo standard yes. Ma la mia idea iniziale sbagliata era che il compilatore avrebbe ottimizzato questa copia. Mi sbagliavo.
  3. Non è (probabilmente) un problema del compilatore poiché sia ​​clang che gcc producono codice che produce un memcpy .
  4. Il memcpy può essere economico, ma non riesco a immaginare circostanze in cui copiare la memoria ed eliminare l'originale è più economico che passare un puntatore con uno std :: move .
  5. L'aggiunta di std :: move è un'operazione elementare. Immagino che un analizzatore di codice sarebbe in grado di suggerire questa correzione.

2
Non posso rispondere se esiste o meno un metodo / strumento per rilevare operazioni di copia "spurie", tuttavia, secondo la mia onesta opinione, non sono d'accordo sul fatto che la copia di std::vectorqualsiasi mezzo non sia ciò che pretende di essere . Il tuo esempio mostra una copia esplicita ed è naturale e l'approccio corretto (di nuovo imho) applicare la std::movefunzione come ti suggerisci se una copia non è quella che desideri. Si noti che alcuni compilatori potrebbero omettere la copia se i flag di ottimizzazione sono attivati ​​e il vettore è invariato.
magnus

Temo che ci siano troppe copie non necessarie (che potrebbero non avere un impatto) per rendere utilizzabile questa regola della linter: - / ( ruggine usa mossa di default quindi richiede una copia esplicita :))
Jarod42

I miei suggerimenti per l'ottimizzazione del codice sono fondamentalmente di smontare la funzione che si desidera ottimizzare e scoprirai le operazioni di copia extra
camp0

Se capisco correttamente il tuo problema, vuoi rilevare i casi in cui un'operazione di copia (costruttore o operatore di assegnazione) viene invocata su un oggetto a seguito della sua distruzione. Per le classi personalizzate, posso immaginare di aggiungere un set di flag di debug quando viene eseguita una copia, ripristinare in tutte le altre operazioni e archiviare il distruttore. Tuttavia, non sai come fare lo stesso per le classi non personalizzate a meno che tu non sia in grado di modificare il loro codice sorgente.
Daniel Langr,

2
La tecnica che uso per trovare copie spurie è rendere temporaneamente privato il costruttore di copie, quindi esaminare dove il compilatore si oppone a causa delle restrizioni di accesso. (Lo stesso obiettivo può essere raggiunto etichettando il costruttore di copie come obsoleto, per i compilatori che supportano tale codifica.)
Eljay

Risposte:


2

Credo che tu abbia l'osservazione corretta ma l'interpretazione sbagliata!

La copia non verrà restituita restituendo il valore, poiché in questo caso ogni normale compilatore intelligente utilizzerà (N) RVO . Da C ++ 17 questo è obbligatorio, quindi non puoi vedere alcuna copia restituendo un vettore generato locale dalla funzione.

OK, giochiamo un po 'con std::vectore cosa accadrà durante la costruzione o riempiendolo passo dopo passo.

Prima di tutto, generiamo un tipo di dati che rende ogni copia o spostamento visibile come questo:

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; }

};

E ora iniziamo alcuni esperimenti:

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; }
}

Cosa possiamo osservare:

Esempio 1) Creiamo un vettore da un elenco di inizializzatori e forse ci aspettiamo di vedere 4 volte il costrutto e 4 mosse. Ma ne otteniamo 4 copie! Sembra un po 'misterioso, ma il motivo è l'implementazione dell'elenco di inizializzatori! Semplicemente non è consentito spostarsi dall'elenco poiché l'iteratore dall'elenco è un const T*elemento che rende impossibile spostare elementi da esso. Una risposta dettagliata su questo argomento è disponibile qui: initializer_list e sposta la semantica

Esempio 2) In questo caso, otteniamo una costruzione iniziale e 4 copie del valore. Non è niente di speciale ed è ciò che possiamo aspettarci.

Esempio 3) Anche qui, abbiamo la costruzione e alcune mosse come previsto. Con la mia implementazione stl il vettore cresce ogni volta di fattore 2. Quindi vediamo un primo costrutto, un altro e poiché il vettore si ridimensiona da 1 a 2, vediamo lo spostamento del primo elemento. Aggiungendo il 3, vediamo un ridimensionamento da 2 a 4 che richiede uno spostamento dei primi due elementi. Tutto come previsto!

Esempio 4) Ora riserviamo spazio e riempiamo in seguito. Ora non abbiamo più copia e nessuna mossa!

In tutti i casi, non vediamo alcuna mossa né copia restituendo il vettore al chiamante! (N) RVO è in corso e non sono necessarie ulteriori azioni in questo passaggio!

Torna alla tua domanda:

"Come trovare operazioni di copia spuria in C ++"

Come visto sopra, è possibile introdurre una classe proxy tra a scopo di debug.

Rendere privato il copy-ctor potrebbe non funzionare in molti casi, poiché potresti avere alcune copie desiderate e alcune nascoste. Come sopra, solo il codice per esempio 4 funzionerà con un copy-ctor privato! E non posso rispondere alla domanda, se l'esempio 4 è il più veloce, poiché riempiamo la pace con la pace.

Mi dispiace che non posso offrire una soluzione generale per trovare copie "indesiderate" qui. Anche se digiti il ​​tuo codice per le chiamate di memcpy, non troverai tutto, poiché anche memcpytu sarai ottimizzato e vedrai direttamente alcune istruzioni dell'assemblatore che fanno il lavoro senza una chiamata alla tua memcpyfunzione di libreria .

Il mio suggerimento non è quello di concentrarsi su un problema così piccolo. Se hai problemi di prestazioni reali, prendi un profiler e misura. Ci sono così tanti potenziali killer delle prestazioni, che investire molto tempo memcpysull'uso spurio non sembra un'idea così utile.


La mia domanda è un po 'accademica. Sì, ci sono molti modi per avere un codice lento e questo non è un problema immediato per me. Tuttavia, possiamo trovare le operazioni memcpy usando il compilatore Explorer. Quindi, c'è sicuramente un modo. Ma è fattibile solo per piccoli programmi. Il mio punto è che esiste un interesse per il codice che potrebbe trovare suggerimenti su come migliorarlo. Esistono analizzatori di codice che rilevano bug e perdite di memoria, perché non tali problemi?
Mathieu Dutour Sikiric,

"codice che troverà suggerimenti su come migliorare il codice." Ciò è già stato fatto e implementato nei compilatori stessi. (N) L'ottimizzazione RVO è solo un singolo esempio e funziona perfettamente come mostrato sopra. La cattura di memcpy non ha aiutato mentre stai cercando "memcpy indesiderato". "Esistono analizzatori di codice che rilevano bug e perdite di memoria, perché non tali problemi?" Forse non è un problema (comune). E anche uno strumento molto più generale per trovare problemi di "velocità" è già presente: profiler! La mia sensazione personale è che stai cercando qualcosa di accademico che oggi non è un problema nel software reale.
Klaus

1

So che si è verificata un'operazione di copia perché ho utilizzato Explorer del compilatore e mostra una chiamata a memcpy.

Hai inserito la tua applicazione completa in Explorer compilatore e hai abilitato le ottimizzazioni? In caso contrario, ciò che hai visto nell'esploratore del compilatore potrebbe o meno essere ciò che sta accadendo con la tua applicazione.

Un problema con il codice che hai pubblicato è che prima crei un std::vectore poi lo copi in un'istanza di data. Sarebbe meglio inizializzare data con il vettore:

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

Inoltre, se dai semplicemente all'esploratore del compilatore la definizione di datae get_vector(), e nient'altro, deve aspettarsi il peggio. Se gli dai effettivamente del codice sorgente che usa get_vector() , allora guarda quale assembly viene generato per quel codice sorgente. Vedi questo esempio per ciò che la modifica di cui sopra, l'utilizzo effettivo e le ottimizzazioni del compilatore possono causare la produzione del compilatore.


Ho solo inserito in computer explorer il codice sopra (che ha il memcpy ) altrimenti la domanda non avrebbe senso. Detto questo, la tua risposta è eccellente nel mostrare diversi modi per produrre codice migliore. Sono disponibili due modi: uso di elettricità statica e inserimento del costruttore direttamente nell'output. Quindi, questi modi potrebbero essere suggeriti da un analizzatore di codice.
Mathieu Dutour Sikiric,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.