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:
- Itu menyembunyikan fungsi pribadi.
- 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.
PCALL
perilaku tidak terdefinisi. Anda tidak dapat melemparkan sebuahA
kePImpl
dan menggunakannya kecuali objek yang mendasarinya sebenarnya bertipePImpl
. Namun, kecuali saya salah, pengguna hanya akan membuat objek bertipeA
.Jawaban:
Nilai jual dari pola Pimpl adalah:
Untuk efek ini, Pimpl klasik terdiri dari tiga bagian:
Antarmuka untuk objek implementasi, yang harus publik, dan menggunakan metode virtual untuk antarmuka:
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:
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?
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 ;-)
sumber
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:
A.cpp:
sumber
Anda bisa menggunakannya
std::aligned_storage
untuk mendeklarasikan penyimpanan untuk jerawat Anda di kelas antarmuka.Dalam implementasinya Anda dapat membuat kelas Pimpl di tempat
_storage
:sumber
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.
sumber
Dengan segala hormat dan bukan dengan niat membunuh kegembiraan ini, saya tidak melihat manfaat praktis apa pun dari perspektif waktu kompilasi. Banyak manfaat
pimpls
yang akan didapat dari menyembunyikan detail tipe yang ditentukan pengguna. Sebagai contoh:... dalam kasus seperti itu, biaya kompilasi terberat berasal dari fakta bahwa, untuk mendefinisikan
Foo
, kita harus mengetahui persyaratan ukuran / penyelarasanBar
(yang berarti kita harus secara rekursif memerlukan definisiBar
).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
#include
beberapa tajuk lainnya karena Anda menyembunyikan detail yang tidak lagi memerlukanstruct/class
definisi tertentu ). Di situlah aslipimpls
dapat 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
friend
didefinisikan 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