Bagaimana menemukan operasi penyalinan palsu C ++?

11

Baru-baru ini, saya memiliki yang berikut ini

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

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

Masalah dengan kode ini adalah ketika struct dibuat salinan terjadi dan solusinya adalah menulis kembali {std :: move (V)}

Apakah ada linter atau penganalisa kode yang akan mendeteksi operasi penyalinan palsu seperti itu? Baik cppcheck, cpplint, atau clang-rapi tidak bisa melakukannya.

EDIT: Beberapa poin untuk memperjelas pertanyaan saya:

  1. Saya tahu bahwa operasi penyalinan terjadi karena saya menggunakan compiler explorer dan ini menunjukkan panggilan ke memcpy .
  2. Saya dapat mengidentifikasi bahwa operasi penyalinan terjadi dengan melihat standar ya. Tapi ide awal saya yang salah adalah bahwa kompiler akan mengoptimalkan salinan ini. Saya salah.
  3. Ini (kemungkinan) bukan masalah kompiler karena dentang dan gcc menghasilkan kode yang menghasilkan memcpy .
  4. Memcpy mungkin murah, tapi saya tidak bisa membayangkan keadaan di mana menyalin memori dan menghapus yang asli lebih murah daripada melewati pointer dengan std :: move .
  5. Menambahkan std :: move adalah operasi dasar. Saya akan membayangkan bahwa penganalisa kode akan dapat menyarankan koreksi ini.
Mathieu Dutour Sikiric
sumber
2
Saya tidak bisa menjawab apakah ada metode / alat apa pun untuk mendeteksi operasi penyalinan "palsu", namun, menurut pendapat saya yang jujur, saya tidak setuju bahwa penyalinan std::vectordengan cara apa pun tidak seperti yang seharusnya . Contoh Anda menunjukkan salinan eksplisit, dan itu wajar, dan pendekatan yang benar, (sekali lagi imho) untuk menerapkan std::movefungsi seperti yang Anda sarankan sendiri jika salinan bukan yang Anda inginkan. Perhatikan bahwa beberapa kompiler dapat menghilangkan penyalinan jika flag optimisasi dihidupkan, dan vektornya tidak berubah.
Magnus
Saya khawatir ada terlalu banyak salinan yang tidak perlu (yang mungkin tidak berdampak) untuk membuat aturan linter ini dapat digunakan: - / ( karat menggunakan bergerak secara default sehingga memerlukan salinan eksplisit :))
Jarod42
Saran saya untuk mengoptimalkan kode pada dasarnya adalah untuk membongkar fungsi yang ingin Anda optimalkan dan Anda akan menemukan operasi penyalinan tambahan
camp0
Jika saya memahami masalah Anda dengan benar, Anda ingin mendeteksi kasus di mana operasi penyalinan (konstruktor atau operator penugasan) dipanggil pada objek berikut dengan kehancurannya. Untuk kelas kustom, saya bisa membayangkan menambahkan beberapa flag debug yang diatur ketika salinan dilakukan, reset dalam semua operasi lain, dan periksa di destructor. Namun, tidak tahu bagaimana melakukan hal yang sama untuk kelas non-kustom kecuali Anda dapat memodifikasi kode sumbernya.
Daniel Langr
2
Teknik yang saya gunakan untuk menemukan salinan palsu adalah untuk sementara membuat salinan konstruktor pribadi, dan kemudian memeriksa di mana kompiler menolak karena pembatasan akses. (Sasaran yang sama dapat dicapai dengan memberi tag pada konstruktor salinan yang sudah usang, untuk kompiler yang mendukung penandaan semacam itu.)
Eljay

Jawaban:

2

Saya percaya Anda memiliki pengamatan yang benar tetapi interpretasi yang salah!

Salinan tidak akan terjadi dengan mengembalikan nilai, karena setiap kompiler pintar yang normal akan menggunakan (N) RVO dalam kasus ini. Dari C ++ 17 ini wajib, jadi Anda tidak dapat melihat salinan apa pun dengan mengembalikan vektor yang dihasilkan lokal dari fungsi.

OK, mari kita bermain sedikit dengan std::vectordan apa yang akan terjadi selama konstruksi atau dengan mengisinya langkah demi langkah.

Pertama-tama, mari kita buat tipe data yang membuat setiap salinan atau memindahkan terlihat seperti ini:

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

};

Dan sekarang mari kita mulai beberapa percobaan:

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

Apa yang bisa kita amati:

Contoh 1) Kami membuat vektor dari daftar penginisialisasi dan mungkin kami berharap kami akan melihat 4 kali konstruk dan 4 gerakan. Tapi kami mendapat 4 salinan! Kedengarannya agak misterius, tetapi alasannya adalah penerapan daftar penginisialisasi! Sederhananya tidak diperbolehkan untuk pindah dari daftar karena iterator dari daftar adalah const T*yang membuatnya tidak mungkin untuk memindahkan elemen dari daftar. Jawaban terperinci tentang topik ini dapat ditemukan di sini: initializer_list dan pindahkan semantik

Contoh 2) Dalam hal ini, kami mendapatkan konstruksi awal dan 4 salinan nilainya. Itu tidak ada yang istimewa dan itulah yang bisa kita harapkan.

Contoh 3) Juga di sini, kita membangun dan beberapa bergerak seperti yang diharapkan. Dengan implementasi stl saya, vektor tumbuh dengan faktor 2 setiap kali. Jadi kita melihat konstruk pertama, yang lain dan karena vektor mengubah ukuran dari 1 menjadi 2, kita melihat pergerakan elemen pertama. Saat menambahkan 3 satu, kita melihat ukuran dari 2 ke 4 yang membutuhkan perpindahan dari dua elemen pertama. Semua seperti yang diharapkan!

Contoh 4) Sekarang kami memesan ruang dan mengisi nanti. Sekarang kami tidak memiliki salinan dan tidak bergerak lagi!

Dalam semua kasus, kami tidak melihat gerakan atau salinan dengan mengembalikan vektor ke pemanggil sama sekali! (N) RVO sedang berlangsung dan tidak ada tindakan lebih lanjut diperlukan dalam langkah ini!

Kembali ke pertanyaan Anda:

"Bagaimana menemukan operasi penyalinan palsu C ++"

Seperti yang terlihat di atas, Anda dapat memperkenalkan kelas proxy di antaranya untuk tujuan debugging.

Menjadikan copy-ctor pribadi mungkin tidak berfungsi dalam banyak kasus, karena Anda mungkin memiliki beberapa salinan yang diinginkan dan beberapa yang tersembunyi. Seperti di atas, hanya kode misalnya 4 yang akan berfungsi dengan copy-ctor pribadi! Dan saya tidak bisa menjawab pertanyaan, jika contoh 4 adalah yang tercepat, karena kami mengisi kedamaian dengan kedamaian.

Maaf saya tidak dapat menawarkan solusi umum untuk menemukan salinan "yang tidak diinginkan" di sini. Bahkan jika Anda menggali kode untuk panggilan memcpy, Anda tidak akan menemukan semuanya juga memcpyakan dioptimalkan dan Anda melihat langsung beberapa instruksi assembler melakukan pekerjaan tanpa panggilan ke memcpyfungsi perpustakaan Anda .

Petunjuk saya adalah jangan fokus pada masalah sekecil itu. Jika Anda memiliki masalah kinerja nyata, ambil profiler dan ukur. Ada begitu banyak potensi kinerja pembunuh, sehingga menginvestasikan banyak waktu untuk memcpypenggunaan palsu tampaknya bukan ide yang berharga.

Klaus
sumber
Pertanyaan saya agak akademis. Ya, ada banyak cara untuk memiliki kode yang lambat dan ini bukan masalah langsung bagi saya. Namun, kita dapat menemukan operasi memcpy dengan menggunakan compiler explorer. Jadi, pasti ada caranya. Tetapi hanya layak untuk program kecil. Maksud saya adalah bahwa ada minat kode yang akan menemukan saran tentang cara meningkatkan kode. Ada penganalisa kode yang menemukan bug dan kebocoran memori, mengapa tidak ada masalah seperti itu?
Mathieu Dutour Sikiric
"kode yang akan menemukan saran tentang cara meningkatkan kode." Itu sudah dilakukan dan diimplementasikan dalam kompiler itu sendiri. (N) optimasi RVO hanya satu contoh dan bekerja sempurna seperti yang ditunjukkan di atas. Menangkap memcpy tidak membantu karena Anda mencari "memcpy yang tidak diinginkan". "Ada penganalisa kode yang menemukan bug dan kebocoran memori, mengapa tidak ada masalah seperti itu?" Mungkin itu bukan masalah (umum). Dan alat yang jauh lebih umum untuk menemukan masalah "kecepatan" juga sudah ada: profiler! Perasaan pribadi saya adalah, bahwa Anda mencari hal akademis yang tidak menjadi masalah dalam perangkat lunak nyata saat ini.
Klaus
1

Saya tahu bahwa operasi penyalinan terjadi karena saya menggunakan compiler explorer dan ini menunjukkan panggilan ke memcpy.

Apakah Anda memasukkan aplikasi lengkap Anda ke dalam compiler explorer, dan apakah Anda mengaktifkan optimasi? Jika tidak, maka apa yang Anda lihat di kompiler explorer mungkin atau mungkin bukan apa yang terjadi dengan aplikasi Anda.

Salah satu masalah dengan kode yang Anda poskan adalah bahwa Anda pertama kali membuat std::vector, dan kemudian menyalinnya ke instance data. Akan lebih baik menginisialisasi data dengan vektor:

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

Juga, jika Anda hanya memberikan definisi kompiler explorer datadan get_vector(), dan tidak ada yang lain, itu harus mengharapkan yang lebih buruk. Jika Anda benar-benar memberikannya beberapa kode sumber yang digunakan get_vector() , maka lihat perakitan apa yang dihasilkan untuk kode sumber itu. Lihat contoh ini untuk apa modifikasi di atas ditambah penggunaan aktual ditambah optimisasi kompiler dapat menyebabkan kompiler menghasilkan.

G. Sliepen
sumber
Saya hanya meletakkan di komputer explorer kode di atas (yang memiliki memcpy ) kalau tidak pertanyaan itu tidak masuk akal. Yang sedang berkata jawaban Anda sangat baik dalam menunjukkan berbagai cara untuk menghasilkan kode yang lebih baik. Anda memberikan dua cara: Penggunaan statis dan meletakkan konstruktor langsung di output. Jadi, cara-cara itu dapat disarankan oleh penganalisa kode.
Mathieu Dutour Sikiric