C ++ sahte kopya işlemleri nasıl bulunur?


11

Son zamanlarda aşağıdakileri yaşadım

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

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

Bu kod ile ilgili sorun, yapı oluşturulduğunda bir kopya oluşması ve çözümün dönüş {std :: move (V)} yazmaktır

Bu tür sahte kopya işlemlerini algılayacak linter veya kod analizörü var mı? Ne cppcheck, cpplint ne de clang-tidy bunu yapamaz.

EDIT: Sorumu netleştirmek için birkaç nokta:

  1. Derleyici gezgini kullandığım ve memcpy'a bir çağrı gösterdiğinden bir kopyalama işleminin gerçekleştiğini biliyorum .
  2. Standart evet'e bakarak bir kopyalama işleminin gerçekleştiğini belirleyebilirim. Ama ilk yanlış fikrim derleyicinin bu kopyayı optimize edeceğiydi. Ben hatalıydım.
  3. Hem clang hem de gcc bir memcpy üreten kod ürettiğinden bir derleyici sorunu (büyük olasılıkla) değildir .
  4. Memcpy ucuz olabilir, ancak bellek kopyalama ve orijinal silme bir std :: move ile bir işaretçi geçmekten daha ucuz olduğu durumlarda hayal edemiyorum .
  5. Std :: move eklenmesi temel bir işlemdir. Bir kod çözümleyicisinin bu düzeltmeyi önerebileceğini hayal ediyorum.

2
"Sahte" kopyalama işlemlerini tespit etmek için herhangi bir yöntem / araç olup olmadığına cevap veremem, ancak dürüst görüşüme göre, kopyalamanın std::vectorherhangi bir şekilde kopyalanmasının amaçlandığı gibi olmadığını kabul etmiyorum . Örneğiniz açık bir kopyayı gösterir ve std::movebir kopya istediğiniz gibi değilse kendinize önerdiğiniz gibi işlevi uygulamak yalnızca doğal ve doğru yaklaşımdır (yine imho) . Optimizasyon bayrakları açıksa ve vektör değiştirilmezse bazı derleyicilerin kopyalamayı atlayabileceğini unutmayın.
magnus

Bu linter kuralını kullanılabilir hale getirmek için çok fazla gereksiz kopya (çok fazla
etkilenmeyebilir

Optimize kodu için önerilerim temel olarak optimize etmek istediğiniz işlevi
sökmektir

Sorununuzu doğru anlarsam, imha işleminin ardından bir nesne üzerinde bir kopyalama işleminin (kurucu veya atama operatörü) çağrıldığı durumları tespit etmek istersiniz. Özel sınıflar için, bir kopya gerçekleştirildiğinde bazı hata ayıklama bayrağı ekleyebilir, diğer tüm işlemlerde sıfırlayabilir ve yıkıcıyı kontrol edebilirim. Ancak, kaynak kodlarını değiştiremediğiniz sürece, özel olmayan sınıflar için de aynısını nasıl yapacağınızı bilmiyorum.
Daniel Langr

2
Sahte kopyaları bulmak için kullandığım teknik, kopya oluşturucuyu geçici olarak özel yapmak ve daha sonra erişim kısıtlamaları nedeniyle derleyicinin nerede balyaladığını incelemek. (Aynı hedefe, bu etiketlemeyi destekleyen derleyiciler için kopya oluşturucuyu kullanımdan kaldırılmış olarak etiketleme yoluyla ulaşılabilir.)
Eljay

Yanıtlar:


2

Doğru gözlemlere ancak yanlış yoruma sahip olduğuna inanıyorum!

Her normal akıllı derleyici bu durumda (N) RVO kullanacağından, değer döndürülerek kopyalama gerçekleşmez . C ++ 17'den bu zorunludur, bu nedenle işlevden yerel olarak üretilen bir vektör döndürerek herhangi bir kopya göremezsiniz.

Tamam, std::vectorinşaat sırasında veya adım adım doldurarak biraz oynayalım ve neler olacak.

Her şeyden önce, her kopyayı veya taşınmayı şu şekilde görünür kılan bir veri türü oluşturalım:

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

};

Ve şimdi bazı deneyler başlatalım:

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

Ne gözlemleyebiliriz:

Örnek 1) Başlatıcı listesinden bir vektör oluşturuyoruz ve belki 4 kez yapı ve 4 hareket göreceğimizi umuyoruz. Ama 4 kopya alıyoruz! Bu biraz gizemli geliyor, ama nedeni başlatıcı listesinin uygulanması! Listeden yineleyici const T*öğelerin taşınmasını imkansız kıldığı için listeden hareket etmesine izin verilmez . Bu konuyla ilgili ayrıntılı bir cevabı burada bulabilirsiniz: initializer_list ve taşıma semantiği

Örnek 2) Bu durumda, başlangıçtaki bir yapıyı ve değerin 4 kopyasını alırız. Bu özel bir şey değil ve ne bekleyebiliriz.

Örnek 3) Ayrıca burada, inşaat ve beklendiği gibi bazı hareketler. Benim stl uygulaması ile vektör her zaman faktör 2 büyür. İlk yapıyı, diğerini görüyoruz ve vektör 1'den 2'ye yeniden boyutlandırdığından, ilk elemanın hareketini görüyoruz. Üçünü eklerken, ilk iki öğenin hareket etmesini gerektiren 2'den 4'e kadar bir yeniden boyutlandırma görüyoruz. Tüm beklendiği gibi!

Örnek 4) Şimdi yer ayırıyoruz ve daha sonra dolduruyoruz. Artık hiçbir kopyamız ve hamlemiz yok!

Her durumda, vektörü arayan kişiye geri döndürerek herhangi bir hareket veya kopya görmüyoruz! (N) RVO gerçekleşiyor ve bu adımda başka bir işlem yapılması gerekmiyor!

Sorunuza geri dönün:

"C ++ sahte kopya işlemleri nasıl bulunur"

Yukarıda görüldüğü gibi, hata ayıklama amacıyla aralarına bir proxy sınıfı ekleyebilirsiniz.

Kopyalayıcıyı özel yapmak pek çok durumda çalışmayabilir, çünkü bazı istediğiniz kopyalara ve gizli olanlara sahip olabilirsiniz. Yukarıdaki gibi, sadece örnek 4'ün kodu özel bir kopyalayıcı ile çalışacaktır! Barışı barışla doldurduğumuz için, eğer örnek 4 en hızlı ise soruya cevap veremem.

Üzgünüm, burada "istenmeyen" kopyaları bulmak için genel bir çözüm sunamıyorum. Kodları aramalar için kazsanız bile memcpy, memcpyoptimize edilecek gibi bulamazsınız ve kitaplık memcpyişlevinizi çağırmadan işi yapan bazı montajcı talimatlarını doğrudan görürsünüz .

İpucu küçük bir soruna odaklanmak değil. Gerçek performans sorunlarınız varsa bir profil oluşturup ölçün. O kadar çok potansiyel performans katili var ki, sahte memcpykullanıma çok fazla zaman harcamak böyle bir fikir değil gibi görünüyor.


Benim sorum biraz akademik. Evet, yavaş kod almanın birçok yolu var ve bu benim için acil bir sorun değil. Ancak, derleyici gezginini kullanarak memcpy işlemlerini bulabiliriz . Yani, kesinlikle bir yolu var. Ancak sadece küçük programlar için mümkündür. Demek istediğim, kodun nasıl geliştirileceğine dair öneriler bulabilecek kod ilgisi. Hata ve bellek sızıntısı bulan kod analizörleri var, neden böyle problemler olmasın?
Mathieu Dutour Sikiric

msgstr "kodun nasıl geliştirileceğine dair öneriler bulabilir." Derleyicilerin kendisinde zaten yapılmış ve uygulanmıştır. (N) RVO optimizasyonu sadece tek bir örnektir ve yukarıda gösterildiği gibi mükemmel çalışır. "İstenmeyen memcpy" araması yaparken memcpy'ı yakalamak yardımcı olmadı. "Hata ve bellek sızıntısı bulan kod analizörleri var, neden böyle problemler olmasın?" Belki (yaygın) bir sorun değildir. Ve "hız" sorunları bulmak için çok daha genel bir araç da zaten mevcut: profiler! Benim kişisel düşüncem, bugün gerçek yazılımda sorun olmayan bir akademik şey aradığınızdır.
Klaus

1

Derleyici gezgini kullandığım ve memcpy'a bir çağrı gösterdiğinden bir kopyalama işleminin gerçekleştiğini biliyorum.

Tam başvurunuzu derleyici gezginine koydunuz ve optimizasyonları etkinleştirdiniz mi? Değilse, derleyici gezgininde gördükleriniz uygulamanızla olan şey olabilir veya olmayabilir.

Gönderdiğiniz kodla ilgili bir sorun, önce bir a oluşturmanız std::vectorve ardından bunu bir örneğine kopyalamanızdır data. Vektör ile başlangıç data yapmak daha iyi olur :

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

Sadece tanımının kaşif derleyici vermek Ayrıca, datave get_vector()ve başka hiçbir daha kötü beklemek zorundadır. Aslında bunu kullanan bir kaynak kodu verirseniz, o kaynak kodu get_vector()için hangi montajın üretildiğine bakın. Yukarıdaki modifikasyon artı gerçek kullanım artı derleyici optimizasyonlarının derleyicinin üretmesine neden olabileceği için bu örneğe bakın .


Ben sadece bilgisayar kaşif yukarıdaki kodu (bu memcpy vardır ) koymak aksi takdirde soru anlamlı olmaz. Bununla birlikte, cevabınız daha iyi kod üretmenin farklı yollarını göstermede mükemmeldir. İki yol sağlarsınız: Statik kullanımı ve yapıcıyı doğrudan çıktıya yerleştirme. Yani, bu yollar bir kod analizörü tarafından önerilebilir.
Mathieu Dutour Sikiric
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.