Bisakah variasi jerawat diterapkan tanpa penalti kinerja?

9

Salah satu masalah pimpl adalah penalti kinerja menggunakannya (alokasi memori tambahan, anggota data yang tidak bersebelahan, tipuan tambahan, dll.). Saya ingin mengusulkan variasi pada idiom jerawat yang akan menghindari hukuman kinerja ini dengan mengorbankan tidak mendapatkan semua manfaat jerawat. Idenya adalah untuk meninggalkan semua anggota data pribadi di kelas itu sendiri dan hanya memindahkan metode pribadi ke kelas pimpl. Manfaat dibandingkan dengan jerawat dasar adalah bahwa memori tetap berdekatan (tidak ada tipuan tambahan). Manfaat dibandingkan dengan tidak menggunakan jerawat sama sekali adalah:

  1. Itu menyembunyikan fungsi pribadi.
  2. Anda dapat menyusunnya sehingga semua fungsi ini akan memiliki tautan internal dan memungkinkan kompiler untuk lebih mengoptimalkannya secara agresif.

Jadi ide saya adalah membuat mucikari mewarisi dari kelas itu sendiri (kedengarannya agak gila saya tahu, tapi tahan dengan saya). Akan terlihat seperti ini:

Dalam file Ah:

class A
{
    A();
    void DoSomething();
protected:  //All private stuff have to be protected now
    int mData1;
    int mData2;
//Not even a mention of a PImpl in the header file :)
};

Dalam file A.cpp:

#define PCALL (static_cast<PImpl*>(this))

namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
    static_assert(sizeof(PImpl) == sizeof(A), 
                  "Adding data members to PImpl - not allowed!");
    void DoSomething1();
    void DoSomething2();
    //No data members, just functions!
};

void PImpl::DoSomething1()
{
    mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
    DoSomething2();
}

void PImpl::DoSomething2()
{
    mData2 = baz();
}

}
A::A(){}

void A::DoSomething()
{
    mData2 = foo();
    PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}

Sejauh yang saya lihat sama sekali tidak ada hukuman kinerja dalam menggunakan ini vs no pimpl dan beberapa kemungkinan peningkatan kinerja dan antarmuka file header bersih. Salah satu kelemahan ini memiliki vs standar jerawat adalah bahwa Anda tidak dapat menyembunyikan anggota data sehingga perubahan pada anggota data tersebut masih akan memicu kompilasi ulang semua yang tergantung pada file header. Tetapi menurut saya, itu bisa mendapatkan manfaat itu atau manfaat kinerja karena memiliki anggota yang berdekatan dalam memori (atau melakukan peretasan ini)- "Mengapa Percobaan # 3 Disesalkan"). Peringatan lain adalah bahwa jika A adalah kelas templated maka sintaksanya akan mengganggu (Anda tahu, Anda tidak dapat menggunakan mData1 secara langsung, Anda perlu melakukan ini-> mData1, dan Anda harus mulai menggunakan nama ketik dan mungkin kata kunci templat untuk jenis dependen dan tipe templated, dll.). Namun peringatan lain adalah bahwa Anda tidak dapat lagi menggunakan pribadi di kelas asli, hanya anggota yang dilindungi, sehingga Anda tidak dapat membatasi akses dari kelas warisan apa pun, tidak hanya jerawat. Saya mencoba tetapi tidak bisa menyelesaikan masalah ini. Sebagai contoh, saya mencoba membuat pimpl sebagai kelas templat teman dengan harapan membuat deklarasi teman cukup luas untuk memungkinkan saya mendefinisikan kelas pimpl yang sebenarnya di namespace anonim, tetapi itu tidak berhasil. Jika ada yang tahu bagaimana menjaga agar data anggota tetap pribadi dan masih memungkinkan kelas pimpl bawaan yang didefinisikan dalam ruang nama anonim untuk mengaksesnya, saya benar-benar ingin melihatnya! Itu akan menghilangkan reservasi utama saya dari menggunakan ini.

Saya merasa, bahwa peringatan ini dapat diterima untuk manfaat dari apa yang saya usulkan.

Saya mencoba mencari online untuk beberapa referensi untuk idiom "fungsi-hanya jerawat" ini tetapi tidak dapat menemukan apa pun. Saya sangat tertarik dengan apa yang orang pikirkan tentang ini. Apakah ada masalah lain dengan ini atau alasan saya tidak boleh menggunakan ini?

MEMPERBARUI:

Saya telah menemukan proposal ini yang sedikit banyak berusaha mencapai apa yang sebenarnya saya lakukan, tetapi melakukannya dengan mengubah standar. Saya sepenuhnya setuju dengan proposal itu dan berharap itu akan menjadi standar (saya tidak tahu apa-apa tentang proses itu jadi saya tidak tahu bagaimana kemungkinan itu terjadi). Saya lebih suka melakukan ini melalui mekanisme bahasa bawaan. Proposal juga menjelaskan manfaat dari apa yang saya coba capai jauh lebih baik daripada saya. Itu juga tidak memiliki masalah melanggar enkapsulasi seperti saran saya (pribadi -> dilindungi). Tetap saja, sampai proposal itu membuatnya menjadi standar (jika itu pernah terjadi), saya pikir saran saya memungkinkan untuk mendapatkan manfaat itu, dengan peringatan yang saya cantumkan.

UPDATE2:

Salah satu jawaban menyebutkan KPP sebagai alternatif yang mungkin untuk mendapatkan beberapa manfaat (optimisasi lebih agresif saya kira). Saya tidak benar-benar yakin apa yang terjadi di berbagai optimisasi kompiler melewati tapi saya punya sedikit pengalaman dengan kode yang dihasilkan (saya menggunakan gcc). Cukup dengan meletakkan metode pribadi di kelas asli akan memaksa mereka untuk memiliki hubungan eksternal.

Saya mungkin salah di sini, tetapi cara saya menafsirkannya adalah bahwa pengoptimal waktu kompilasi tidak dapat menghilangkan fungsi bahkan jika semua instance panggilannya benar-benar diuraikan di dalam TU itu. Untuk beberapa alasan bahkan KPP menolak untuk menyingkirkan definisi fungsi bahkan jika tampaknya semua contoh panggilan dalam seluruh biner yang terhubung semuanya digarisbawahi. Saya menemukan beberapa referensi yang menyatakan bahwa itu karena linker tidak tahu apakah Anda masih akan memanggil fungsi menggunakan pointer fungsi (meskipun saya tidak mengerti mengapa linker tidak dapat mengetahui bahwa alamat metode itu tidak pernah diambil ).

Ini tidak terjadi jika Anda menggunakan saran saya dan menempatkan metode pribadi di jerawat di dalam namespace anonim. Jika itu diuraikan, fungsi TIDAK akan muncul di (dengan -O3, yang mencakup -finline-fungsi) file objek.

Cara saya memahaminya, pengoptimal, ketika memutuskan apakah akan fungsi inline atau tidak, memperhitungkan dampaknya pada ukuran kode. Jadi, dengan menggunakan saran saya, saya membuatnya sedikit "lebih murah" bagi pengoptimal untuk menyesuaikan metode pribadi tersebut.

dcmm88
sumber
Penggunaan PCALLperilaku tidak terdefinisi. Anda tidak dapat melemparkan sebuah Ake PImpldan menggunakannya kecuali objek yang mendasarinya sebenarnya bertipe PImpl. Namun, kecuali saya salah, pengguna hanya akan membuat objek bertipe A.
BeeOnRope

Jawaban:

8

Nilai jual dari pola Pimpl adalah:

  • enkapsulasi total: tidak ada anggota data (pribadi) yang disebutkan dalam file header objek antarmuka.
  • stabilitas: sampai Anda memecahkan antarmuka publik (yang dalam C ++ termasuk anggota pribadi), Anda tidak perlu mengkompilasi ulang kode yang tergantung pada objek antarmuka. Ini membuat Pimpl pola yang bagus untuk pustaka yang tidak ingin penggunanya mengkompilasi ulang semua kode pada setiap perubahan internal.
  • polimorfisme dan injeksi dependensi: implementasi atau perilaku objek antarmuka dapat dengan mudah ditukar saat runtime, tanpa memerlukan kode dependen untuk dikompilasi ulang. Sangat bagus jika Anda perlu mengejek sesuatu untuk unit test.

Untuk efek ini, Pimpl klasik terdiri dari tiga bagian:

  • Antarmuka untuk objek implementasi, yang harus publik, dan menggunakan metode virtual untuk antarmuka:

    class IFrobnicateImpl
    {
    public:
        virtual int frobnicate(int) const = 0;
    };

    Antarmuka ini harus stabil.

  • Objek antarmuka yang proksi ke implementasi pribadi. Tidak harus menggunakan metode virtual. Satu-satunya anggota yang diizinkan adalah pointer ke implementasi:

    class Frobnicate
    {
        std::unique_ptr<IFrobnicateImpl> _impl;
    public:
        explicit Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl = nullptr);
        int frobnicate(int x) const { return _impl->frobnicate(x); }
    };
    
    ...
    
    Frobnicate::Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl /* = nullptr */)
    : _impl(std::move(impl))
    {
        if (!_impl)
            _impl = std::make_unique<DefaultImplementation>();
    }

    File header kelas ini harus stabil.

  • Setidaknya satu implementasi

Pimpl kemudian membeli kami banyak stabilitas untuk kelas perpustakaan, dengan biaya satu alokasi tumpukan dan pengiriman virtual tambahan.

Bagaimana solusi Anda mengukur?

  • Itu tidak jauh dengan enkapsulasi. Karena anggota Anda dilindungi, setiap subclass dapat mengacaukannya.
  • Itu tidak jauh dengan stabilitas antarmuka. Setiap kali Anda mengubah anggota data Anda - dan perubahan itu hanya berjarak satu refactoring - Anda harus mengkompilasi ulang semua kode dependen.
  • Itu tidak jauh dengan lapisan pengiriman virtual, mencegah pertukaran yang mudah dari implementasi.

Jadi untuk setiap tujuan dari pola Pimpl, Anda gagal memenuhi tujuan ini. Oleh karena itu, tidak masuk akal untuk menyebut pola Anda sebagai variasi dari Pimpl, itu lebih merupakan kelas biasa. Sebenarnya, ini lebih buruk daripada kelas biasa karena variabel anggota Anda bersifat pribadi. Dan karena itu pemain yang merupakan titik kerapuhan mencolok.

Perhatikan bahwa pola Pimpl tidak selalu optimal - ada pertukaran antara stabilitas dan polimorfisme di satu sisi, dan kekompakan memori di sisi lain. Secara semantik tidak mungkin suatu bahasa memiliki keduanya (tanpa kompilasi JIT). Jadi, jika Anda mengoptimalkan mikro untuk kekompakan memori, jelas Pimpl bukan solusi yang cocok untuk kasus penggunaan Anda. Anda mungkin juga akan berhenti menggunakan setengah dari perpustakaan standar, karena kelas string dan vektor yang mengerikan ini melibatkan alokasi memori dinamis ;-)

amon
sumber
Tentang catatan terakhir Anda tentang penggunaan string dan vektor - sangat dekat dengan kebenaran :). Saya mengerti bahwa jerawat memiliki manfaat yang tidak saya sarankan. Memang ada manfaatnya, yang lebih fasih disajikan dalam tautan yang saya tambahkan di UPDATE. Mengingat kinerja memainkan peran yang sangat besar dalam kasus penggunaan saya, bagaimana Anda membandingkan saran saya dengan tidak menggunakan jerawat sama sekali? Karena untuk kasus penggunaan saya itu bukan saran saya vs pimpl, itu saran saya vs no-pimpl sama sekali, karena pimpl memiliki biaya kinerja yang tidak ingin saya keluarkan.
dcmm88
1
@ dcmm88 Jika kinerja adalah tujuan # 1 Anda, maka jelas hal-hal seperti kualitas kode dan enkapsulasi adalah permainan yang adil. Satu-satunya solusi yang Anda perbaiki dari kelas normal adalah menghindari kompilasi ulang ketika tanda tangan metode pribadi berubah. Dalam buku saya, itu tidak terlalu banyak, tetapi jika ini adalah kelas dasar dalam basis kode yang besar, implementasi aneh mungkin sepadan. Konsultasikan bagan XKCD yang praktis ini untuk menentukan berapa banyak waktu pengembangan yang dapat Anda pergunakan untuk mempersingkat waktu kompilasi . Tergantung pada gaji Anda, memutakhirkan komputer Anda mungkin lebih murah.
amon
1
Ini juga memungkinkan fungsi pribadi untuk memiliki tautan internal, memungkinkan untuk pengoptimalan yang lebih agresif.
dcmm88
1
Tampaknya Anda mengekspos antarmuka publik dari kelas pimpl di file header dari kelas asli. AFAUI seluruh idenya adalah untuk menyembunyikan sebanyak mungkin. Biasanya cara saya melihat pimpl diimplementasikan, file header hanya memiliki deklarasi maju dari kelas pimpl dan cpp memiliki definisi lengkap dari kelas pimpl. Saya pikir kemampuan bertukar datang dengan pola desain yang berbeda - pola jembatan. Jadi, saya tidak yakin itu adil untuk mengatakan saran saya gagal menyediakannya, ketika pimpl biasanya tidak memberikannya.
dcmm88
Maaf atas komentar terakhir. Anda sebenarnya memiliki jawaban yang cukup besar, dan intinya adalah pada akhirnya, jadi saya melewatkannya
BЈовић
3

Bagi saya, keuntungannya tidak melebihi kerugiannya.

Keuntungan :

Itu dapat mempercepat kompilasi, karena menghemat membangun kembali jika hanya tanda tangan metode pribadi yang berubah. Tetapi pembangunan kembali diperlukan jika tanda tangan metode publik atau terlindungi atau anggota data pribadi telah berubah, dan jarang saya harus mengubah tanda tangan metode pribadi tanpa menyentuh salah satu opsi ini.

Ini dapat memungkinkan optimisasi kompiler yang lebih agresif, tetapi KPP harus mengizinkan banyak optimisasi yang sama (setidaknya, saya pikir itu bisa - saya bukan guru optimisasi kompiler), ditambah beberapa lagi, dan dapat dibuat standar dan otomatis.

Kekurangan:

Anda menyebutkan beberapa kelemahan: ketidakmampuan untuk menggunakan pribadi, dan kompleksitas dengan template. Namun, bagi saya, kerugian terbesar adalah canggung: gaya pemrograman yang tidak konvensional, dengan lompatan bergaya jerawat yang tidak terlalu standar antara antarmuka dan implementasi, yang akan asing bagi pengelola masa depan atau anggota tim baru, dan yang mungkin sangat tidak didukung oleh tooling (lihat, misalnya, bug GDB ini ).

Masalah standar tentang pengoptimalan berlaku di sini: Sudahkah Anda mengukur bahwa pengoptimalan memberikan peningkatan berarti pada kinerja Anda? Apakah kinerja Anda lebih baik dengan melakukan ini atau dengan meluangkan waktu untuk mempertahankannya dan menginvestasikannya dalam membuat profil hotspot, meningkatkan algoritme, dll.? Secara pribadi, saya lebih suka memilih gaya pemrograman yang jelas dan mudah, dengan asumsi bahwa itu akan meluangkan waktu untuk melakukan optimasi yang ditargetkan. Tapi itu perspektif saya untuk jenis kode yang saya kerjakan - untuk domain masalah Anda, pengorbanannya mungkin berbeda.

Catatan tambahan: Mengizinkan pribadi dengan jerawat metode saja

Anda bertanya tentang bagaimana mengizinkan anggota pribadi dengan saran jerawat metode khusus Anda. Sayangnya, saya menganggap jerawat hanya metode sebagai jenis peretasan, tetapi jika Anda telah memutuskan bahwa keuntungan lebih besar daripada kerugiannya, maka Anda mungkin juga merangkul peretasan tersebut.

Ah:

#ifndef A_impl
#define A_impl private
#endif

class A
{
public:
    A();
    void DoSomething();
A_impl:
    int mData1;
    int mData2;
};

A.cpp:

#define A_impl public
#include "A.h"
Josh Kelley
sumber
Saya agak malu untuk mengatakannya, tapi saya suka saran pribadi #define A_impl Anda :). Saya juga bukan guru pengoptimalan, tetapi saya memiliki pengalaman dengan KPP dan mengutak-atik itu yang membuat saya memunculkan ini. Saya akan memperbarui pertanyaan saya dengan info yang saya miliki tentang penggunaannya dan mengapa itu masih tidak memberikan manfaat penuh yang diberikan oleh metode jerawat saya saja.
dcmm88
@ dcmm88 - Terima kasih atas info tentang KPP. Itu bukan sesuatu yang saya punya banyak pengalaman.
Josh Kelley
1

Anda bisa menggunakannya std::aligned_storageuntuk mendeklarasikan penyimpanan untuk jerawat Anda di kelas antarmuka.

class A
{
std::aligned_storage< 128 > _storage;
public:
  A();
};

Dalam implementasinya Anda dapat membuat kelas Pimpl di tempat _storage:

class Pimpl
{
  int _some_data;
};

A::A()
{
  ::new(&_storage) Pimpl();
  // accessing Pimpl: *(Pimpl*)(_storage);
}

A::~A()
{
  ((Pimpl*)(_storage))->~Pimpl(); // calls destructor for inline pimpl
}
Fabio
sumber
Lupa menyebutkan Anda perlu menambah ukuran penyimpanan sesekali, sehingga memaksa kompilasi ulang. Anda sering dapat memeras banyak perubahan yang tidak memicu kompilasi ulang - jika Anda memberikan sedikit kelonggaran penyimpanan.
Fabio
0

Tidak, Anda tidak dapat menerapkannya tanpa penalti kinerja. PIMPL, pada dasarnya, adalah penalti kinerja, karena Anda menerapkan tipuan run-time.

Tentu saja, ini tergantung pada apa yang Anda inginkan secara tidak langsung. Beberapa informasi sama sekali tidak digunakan oleh konsumen - seperti apa sebenarnya yang ingin Anda masukkan ke dalam 64 byte 4-byte-aligned bytes Anda. Tetapi informasi lain adalah - seperti fakta bahwa Anda menginginkan 64 byte byte-aligned untuk objek Anda.

PIMPL generik tanpa penalti kinerja tidak ada dan tidak akan pernah ada. Ini adalah informasi yang sama yang Anda tolak kepada pengguna yang ingin mereka gunakan untuk mengoptimalkan. Jika Anda memberikannya kepada mereka, IMPL Anda tidak diabstraksikan; jika Anda menolaknya, mereka tidak dapat mengoptimalkan. Anda tidak dapat memiliki keduanya.

DeadMG
sumber
Saya memang memberikan saran "sebagian jerawat" yang saya klaim tidak memiliki penalti kinerja, dan kemungkinan peningkatan kinerja. Anda mungkin membenci saya menyebutnya apa pun yang berhubungan dengan jerawat, tetapi saya menyembunyikan beberapa detail implementasi (metode pribadi). Saya bisa menyebutnya menyembunyikan implementasi metode pribadi, tetapi memilih untuk menyebutnya variasi pimpl, atau metode hanya pimpl.
dcmm88
0

Dengan segala hormat dan bukan dengan niat membunuh kegembiraan ini, saya tidak melihat manfaat praktis apa pun dari perspektif waktu kompilasi. Banyak manfaat pimplsyang akan didapat dari menyembunyikan detail tipe yang ditentukan pengguna. Sebagai contoh:

struct Foo
{
    Bar data;
};

... dalam kasus seperti itu, biaya kompilasi terberat berasal dari fakta bahwa, untuk mendefinisikan Foo, kita harus mengetahui persyaratan ukuran / penyelarasan Bar(yang berarti kita harus secara rekursif memerlukan definisi Bar).

Jika Anda tidak menyembunyikan anggota data, maka salah satu manfaat paling signifikan dari perspektif waktu kompilasi hilang. Juga ada beberapa kode yang berpotensi berbahaya di sana, tetapi tajuknya tidak semakin terang dan file sumber semakin berat dengan fungsi penerusan yang lebih banyak, sehingga cenderung meningkatkan, bukannya mengurangi, waktu kompilasi secara keseluruhan.

Header Ringan adalah Kuncinya

Untuk mendapatkan penurunan waktu pembuatan, Anda ingin dapat menunjukkan teknik yang menghasilkan tajuk berat yang lebih ringan secara dramatis (biasanya dengan memungkinkan Anda untuk tidak lagi secara rekursif #includebeberapa tajuk lainnya karena Anda menyembunyikan detail yang tidak lagi memerlukan struct/classdefinisi tertentu ). Di situlah asli pimplsdapat memiliki efek signifikan, memutus rantai kaskade inklusi header dan menghasilkan header yang jauh lebih independen dengan semua detail pribadi disembunyikan.

Cara yang Lebih Aman

Jika Anda ingin melakukan sesuatu seperti ini, itu juga akan jauh lebih mudah untuk menggunakan yang frienddidefinisikan dalam file sumber Anda daripada yang mewarisi kelas yang sama yang Anda tidak benar-benar instantiate dengan trik casting pointer untuk memanggil metode pada objek tidak terinstalasi, atau cukup gunakan fungsi yang berdiri sendiri dengan tautan internal di dalam file sumber yang menerima parameter yang sesuai untuk melakukan pekerjaan yang diperlukan (salah satu dari ini setidaknya dapat memungkinkan Anda untuk menyembunyikan beberapa metode pribadi dari header untuk penghematan yang sangat sepele. dalam waktu kompilasi dan sedikit ruang gerak untuk menghindari kompilasi cascading).

Memperbaiki Allocator

Jika Anda ingin jenis jerawat termurah, trik utamanya adalah menggunakan pengalokasi tetap. Terutama ketika menjumlahkan pimpl dalam jumlah besar, pembunuh terbesar adalah hilangnya lokalitas spasial dan kesalahan halaman wajib tambahan saat mengakses pimpl untuk pertama kalinya. Dengan preallocating pool memori yang pool off memory ke mucikari yang dialokasikan dan mengembalikan memori ke pool bukannya membebaskannya pada deallokasi, biaya satu kapal contoh kapal mucikari berkurang secara dramatis. Ini masih tidak gratis meskipun dari sudut pandang kinerja, tetapi jauh lebih murah dan jauh lebih ramah cache / halaman.


sumber