Untuk secara dinamis mengaitkan dan memisahkan data dengan cepat terlepas dari masa node QT sedangkan QT yang dikombinasikan dengan kamera memiliki pengetahuan tentang kapan data harus dikaitkan / dipisahkan dengan cepat, itu agak sulit untuk digeneralisasi dan saya pikir solusi Anda adalah sebenarnya tidak buruk. Itu hal yang sulit untuk didisain dengan cara yang sangat bagus dan umum. seperti, "uhh .... uji dengan baik dan kirimkan!" Oke, sedikit lelucon. Saya akan mencoba menawarkan beberapa pemikiran untuk dijelajahi. Salah satu hal yang paling mencolok bagi saya adalah di sini:
void nodeCreated(Node& node)
{
...
// One more thing, The QuadTree actually needs one field of
// Data to continue, so I fill it there
node.xxx = data.xxx
}
Ini memberitahu saya bahwa simpul ref / pointer tidak hanya digunakan sebagai kunci ke wadah asosiatif eksternal. Anda sebenarnya sedang mengakses dan memodifikasi internal node quadtree di luar quadtree itu sendiri. Dan harus ada cara yang cukup mudah untuk setidaknya menghindari itu sebagai permulaan. Jika itu satu-satunya tempat Anda memodifikasi simpul internal di luar quadtree, maka Anda mungkin dapat melakukan ini (katakanlah xxx
adalah pasangan pelampung):
std::pair<float, float> nodeCreated(const Node& node)
{
Data data;
...
map[&node] = data;
...
return data.xxx;
}
Pada titik mana quadtree dapat menggunakan nilai pengembalian fungsi ini untuk menetapkan xxx
. Itu sudah melonggarkan kopling sedikit ketika Anda tidak lagi mengakses internal simpul pohon di luar pohon.
Menghilangkan kebutuhan untuk Terrain
mengakses internal quadtree sebenarnya akan menghilangkan satu-satunya tempat di mana Anda menggabungkan hal-hal yang sangat tidak perlu. Ini adalah satu-satunya PITA nyata jika Anda menukar dengan implementasi GPU, misalnya, karena implementasi GPU mungkin menggunakan rep internal yang sama sekali berbeda untuk node.
Tetapi untuk masalah kinerja Anda, dan di sana saya memiliki lebih banyak pemikiran daripada bagaimana Anda secara maksimal mencapai decoupling dengan hal semacam ini, saya benar-benar akan menyarankan representasi yang sangat berbeda di mana Anda dapat mengubah asosiasi data / disosiasi menjadi operasi waktu konstan yang murah. Agak sulit untuk menjelaskan kepada seseorang yang tidak terbiasa membuat kontainer standar yang memerlukan penempatan baru untuk membangun elemen di tempat dari memori yang terkumpul sehingga saya akan mulai dengan beberapa data:
struct Node
{
....
// Stores an index to the data being associated on the fly
// or -1 if there's no data associated to the node.
int32_t data;
};
class Quadtree
{
private:
// Stores all the data being associated on the fly.
std::vector<char> data;
// Stores the size of the data being associated on the fly.
int32_t type_size;
// Stores an index to the first free index of data
// to reclaim or -1 if the free list is empty.
int32_t free_index;
...
public:
// Creates a quadtree with the specified type size for the
// data associated and disassociated on the fly.
explicit Quadtree(int32_t itype_size): type_size(itype_size), free_data(-1)
{
// Make sure our data type size is at least the size of an integer
// as required for the free list.
if (type_size < sizeof(int32_t))
type_size = sizeof(int32_t);
}
// Inserts a buffer to store a data element and returns an index
// to that.
int32_t alloc_data()
{
int32_t index = free_index;
if (free_index != -1)
{
// If a free index is available, pop it off the
// free list (stack) and return that.
void* mem = data.data() + index * type_size;
free_index = *static_cast<int*>mem;
}
else
{
// Otherwise insert the buffer for the data
// and return an index to that.
index = data.size() / type_size;
data.resize(data.size() + type_size);
}
return index;
}
// Frees the memory for the nth data element.
void free_data(int32_t n)
{
// Push the nth index to the free list to make
// it available for use in subsequent insertions.
void* mem = data.data() + n * type_size;
*static_cast<int*>(mem) = free_index;
free_index = n;
}
...
};
Pada dasarnya itu adalah "daftar gratis yang diindeks". Tetapi ketika Anda menggunakan perwakilan ini untuk data terkait, Anda dapat melakukan sesuatu seperti ini:
class QTInterface
{
virtual std::pair<float, float> createData(void* mem) = 0;
virtual void destroyData(void* mem) = 0;
};
void Quadtree::update(Camera camera)
{
...
node.data = alloc_data();
node.xxx = i.createData(data.data() + node.data * type_size);
...
i.destroyData(data.data() + node.data * type_size);
free_data(node.data);
node.data = -1;
...
}
class Terrain : public QTInterface
{
// Note that we don't even need access to nodes anymore,
// not even as keys to use. We've completely decoupled
// terrains from tree internals.
std::pair<float, float> createData(void* mem) override
{
// Construct the data (placement new) using the memory
// allocated by the tree.
Data* data = new(mem) Data(...);
// Return data to assign to node.xxx.
return data->xxx;
}
void destroyData(void* mem) override
{
// Destroy the data.
static_cast<Data*>(mem)->~Data();
}
};
Semoga ini semua masuk akal, dan tentu saja ini sedikit lebih terpisah dari desain asli Anda karena tidak mengharuskan klien untuk memiliki akses internal ke bidang simpul pohon (sekarang bahkan tidak lagi memerlukan pengetahuan tentang node apa pun, bahkan tidak digunakan sebagai kunci ), dan itu jauh lebih efisien karena Anda dapat mengaitkan dan memisahkan data ke / dari node dalam waktu-konstan (dan tanpa menggunakan tabel hash yang akan menyiratkan konstanta yang jauh lebih besar). Saya berharap data Anda dapat disejajarkan menggunakan max_align_t
(tidak ada bidang SIMD, mis.) Dan dapat disalin secara sepele, jika tidak semuanya menjadi jauh lebih rumit karena kita akan membutuhkan pengalokasi selaras dan mungkin harus menggulung wadah daftar gratis kami sendiri. Nah, jika Anda hanya memiliki tipe yang tidak dapat disalin sepele dan tidak perlu lebih dari itumax_align_t
, kita dapat menggunakan implementasi penunjuk daftar gratis yang mengumpulkan dan menautkan node yang tidak dikontrol untuk menyimpan K
elemen data masing-masing untuk menghindari keharusan merealokasi blok memori yang ada. Saya dapat menunjukkan bahwa jika Anda membutuhkan alternatif seperti itu.
Ini sedikit maju dan sangat spesifik C ++, mengingat ide mengalokasikan dan membebaskan memori untuk elemen sebagai tugas terpisah dari membangun dan menghancurkan mereka. Tetapi jika Anda melakukannya dengan cara ini, Terrain
menyerap tanggung jawab minimum dan tidak memerlukan pengetahuan internal tentang representasi pohon apa pun lagi, bahkan tidak menangani node yang buram. Namun tingkat kontrol memori ini biasanya yang Anda butuhkan jika Anda ingin merancang struktur data yang paling efisien.
Gagasan mendasar di sana adalah bahwa Anda memiliki klien menggunakan tree pass dalam ukuran tipe data yang ingin mereka kaitkan / pisahkan saat terbang ke quadtree ctor. Kemudian quadtree memiliki tanggung jawab mengalokasikan dan membebaskan memori menggunakan ukuran tipe itu. Kemudian meneruskan tanggung jawab membangun dan menghancurkan data kepada klien menggunakan QTInterface
dan pengiriman dinamis. Oleh karena itu, satu-satunya tanggung jawab di luar pohon yang masih berhubungan dengan pohon itu, adalah membangun dan menghancurkan elemen-elemen dari ingatan yang dialokasikan oleh kuadrat dan mendelokasikannya sendiri. Pada saat itu dependensi menjadi seperti ini:
Yang sangat masuk akal mengingat kesulitan apa yang Anda lakukan dan skala input. Pada dasarnya Anda Terrain
hanya bergantung pada Quadtree
dan QTInterface
, dan tidak lagi internal quadtree atau node-nya. Sebelumnya Anda punya ini:
Dan tentu saja masalah melotot dengan itu, terutama jika Anda sedang mempertimbangkan mencoba implementasi GPU, adalah bahwa ketergantungan dari Terrain
ke Node
, sejak pelaksanaan GPU kemungkinan akan ingin menggunakan rep simpul yang sangat berbeda. Tentu saja jika Anda ingin menjadi SOLID hardcore, Anda akan melakukan sesuatu seperti ini:
... bersama dengan mungkin sebuah pabrik. Tetapi IMO yang total berlebihan (paling INode
tidak total total IMO) dan tidak akan sangat membantu dalam kasus granular seperti fungsi quadtree jika masing-masing memerlukan pengiriman dinamis.
Saya selalu mengalami kesulitan dengan cara memisahkan kelas saya dengan benar. Adakah saran yang bisa saya berikan nanti? (Seperti pertanyaan apa yang harus saya tanyakan pada diri saya misalnya, atau bagaimana Anda memproses? Memikirkan hal ini di atas kertas tampak sangat abstrak bagi saya, dan segera kode sesuatu menghasilkan hasil refactoring kemudian)
Secara umum dan kasar, decoupling sering bermuara pada membatasi jumlah informasi yang dibutuhkan kelas atau fungsi tertentu tentang sesuatu yang lain untuk melakukan hal tersebut.
Saya berasumsi Anda menggunakan C ++ karena tidak ada bahasa lain yang saya tahu memiliki sintaks yang tepat, dan di C ++ mekanisme decoupling yang sangat efektif untuk struktur data adalah template kelas dengan polimorfisme statis jika Anda dapat menggunakannya. Jika Anda mempertimbangkan wadah standar seperti std::vector<T, Alloc>
, vektor tidak digabungkan dengan apa pun yang Anda tentukan untuk T
apa pun. Ini hanya membutuhkan yang T
memenuhi beberapa persyaratan antarmuka dasar seperti itu copy-constructible dan memiliki konstruktor default untuk konstruktor isian dan isian ukuran. Dan itu tidak akan memerlukan perubahan sebagai hasil dari T
perubahan.
Jadi, dengan mengaitkannya dengan hal di atas, ini memungkinkan struktur data untuk diimplementasikan menggunakan pengetahuan minimal yang absolut tentang apa yang dikandungnya, dan yang memisahkannya sejauh tidak memerlukan informasi jenis apa pun di muka (terlebih dahulu ada berbicara dalam hal dependensi kode / kopling, bukan informasi waktu kompilasi) tentang apa T
itu.
Cara paling praktis kedua untuk meminimalkan jumlah informasi yang diperlukan adalah dengan menggunakan polimorfisme dinamis. Misalnya, jika Anda ingin menerapkan struktur data yang cukup umum yang meminimalkan pengetahuan tentang apa yang disimpannya, maka Anda mungkin menangkap persyaratan antarmuka untuk apa yang disimpan dalam satu atau lebih antarmuka:
// Contains all the functions (pure virtual) required of the elements
// stored in the container.
class IElement {...};
Tapi bagaimanapun juga intinya untuk meminimalkan jumlah informasi yang Anda butuhkan di muka dengan pengkodean ke antarmuka daripada ke detail nyata. Di sini satu-satunya hal besar yang Anda lakukan yang tampaknya memerlukan lebih banyak informasi daripada yang diperlukan adalah bahwa Anda Terrain
harus memiliki informasi lengkap tentang internal dari simpul Quadtree, misalnya Dalam kasus seperti itu, dengan asumsi satu-satunya alasan yang Anda butuhkan yaitu untuk menetapkan sepotong data ke sebuah simpul, kita dapat dengan mudah menghilangkan ketergantungan itu ke internal simpul pohon dengan hanya mengembalikan data yang harus ditugaskan ke simpul dalam abstrak itu QTInterface
.
Jadi jika saya ingin memisahkan sesuatu, saya hanya fokus pada apa yang perlu dilakukan dan membuat antarmuka untuk itu (baik secara eksplisit menggunakan warisan atau implisit menggunakan polimorfisme statis dan pengetikan bebek). Dan Anda sudah melakukan itu sampai batas tertentu dari quadtree itu sendiri menggunakan QTInterface
untuk memungkinkan klien untuk menimpa fungsinya dengan subtipe dan memberikan rincian konkret yang diperlukan untuk quadtree untuk melakukan hal itu. Satu-satunya tempat di mana saya pikir Anda gagal adalah bahwa klien masih memerlukan akses ke internal quadtree. Anda dapat menghindari itu dengan meningkatkan apa QTInterface
yang tepatnya saya sarankan ketika saya membuatnya mengembalikan nilai untuk ditugaskannode.xxx
dalam implementasi quadtree itu sendiri. Jadi itu hanya masalah membuat hal-hal yang lebih abstrak dan antarmuka lebih lengkap sehingga hal-hal tidak memerlukan informasi yang tidak perlu tentang satu sama lain.
Dan dengan menghindari informasi yang tidak perlu ( Terrain
harus tahu tentang Quadtree
simpul internal), Anda sekarang lebih bebas untuk menukar Quadtree
dengan implementasi GPU, misalnya, tanpa mengubah Terrain
implementasinya juga. Hal-hal yang tidak diketahui satu sama lain adalah bebas untuk berubah tanpa mempengaruhi satu sama lain. Jika Anda benar-benar ingin menukar keluar implementasi GPU quadtree dari yang CPU, Anda mungkin pergi sedikit ke arah rute SOLID di atas denganIQuadtree
(membuat quadtree itu sendiri abstrak). Itu datang dengan hit pengiriman dinamis yang mungkin agak mahal dengan kedalaman pohon dan ukuran input yang Anda bicarakan. Jika tidak, setidaknya itu memerlukan jauh lebih sedikit perubahan pada kode jika hal-hal yang menggunakan quadtree tidak perlu tahu tentang representasi simpul internal untuk bekerja. Anda mungkin dapat menukar satu dengan yang lain hanya memperbarui satu baris kode untuk typedef
, misalnya, bahkan jika Anda tidak menggunakan antarmuka abstrak ( IQuadtree
).
Tapi di situlah saya pikir saya memiliki masalah pertama saya. Sebagian besar waktu saya tidak khawatir tentang optimasi sampai saya melihatnya, tetapi saya pikir jika saya harus menambahkan overhead semacam ini untuk memisahkan kelas saya dengan benar, itu karena desainnya memiliki kekurangan.
Belum tentu. Decoupling seringkali menyiratkan pergeseran ketergantungan dari beton ke abstrak. Abstraksi cenderung menyiratkan penalti runtime kecuali jika kompiler menghasilkan kode pada waktu kompilasi untuk pada dasarnya menghilangkan biaya abstraksi saat runtime. Sebagai gantinya Anda mendapatkan lebih banyak ruang bernapas untuk membuat perubahan tanpa mempengaruhi hal-hal lain, tetapi itu sering kali mengekstraksi semacam penalti kinerja kecuali jika Anda menggunakan pembuatan kode.
Sekarang Anda dapat menghilangkan kebutuhan untuk struktur data asosiatif non-sepele (peta / kamus, yaitu) untuk mengaitkan data ke node (atau apa pun) dengan cepat. Dalam kasus di atas saya baru saja membuat node langsung menyimpan indeks ke data yang akan dialokasikan / dibebaskan dengan cepat. Melakukan hal-hal semacam ini tidak banyak terkait dengan mempelajari cara memisahkan hal-hal secara efektif, seperti bagaimana memanfaatkan tata letak memori untuk struktur data secara efektif (lebih banyak di bidang optimisasi murni).
Prinsip dan kinerja SE yang efektif bertentangan satu sama lain pada tingkat yang cukup rendah. Seringkali decoupling akan memecah tata letak memori terpisah untuk bidang yang biasa diakses bersama-sama, mungkin melibatkan lebih banyak alokasi tumpukan, mungkin melibatkan pengiriman yang lebih dinamis, dll. Ini menjadi cepat diremehkan saat Anda bekerja menuju kode tingkat yang lebih tinggi (mis: operasi yang diterapkan ke seluruh gambar, tidak per -pixel operasi ketika perulangan melalui piksel individu), tetapi memang memiliki biaya yang berkisar dari sepele hingga parah tergantung pada berapa banyak biaya yang dikeluarkan dalam kode Anda yang paling kritis, gila seperti melakukan pekerjaan paling ringan di setiap iterasi.
Apakah saya terlalu rumit? Haruskah saya memperpanjang kelas Node, menjadikannya sekumpulan data sebelum digunakan oleh beberapa kelas?
Saya pribadi tidak berpikir itu sangat buruk jika Anda tidak mencoba untuk menggeneralisasi struktur data Anda terlalu banyak, hanya menggunakannya dalam konteks yang sangat terbatas, dan Anda sedang berhadapan dengan konteks yang sangat kritis terhadap kinerja untuk jenis masalah yang Anda alami sebelumnya sudah ditangani. Dalam hal ini Anda akan mengubah quadtree Anda menjadi detail implementasi medan Anda, misalnya, daripada sesuatu yang akan digunakan secara luas dan umum, dengan cara yang sama seseorang mungkin mengubah octree menjadi detail implementasi mesin fisika mereka dengan tidak lagi membedakan ide "antarmuka publik" dari "internal". Mempertahankan invarian yang terkait dengan indeks spasial kemudian berubah menjadi tanggung jawab kelas yang menggunakannya sebagai detail implementasi pribadi.
Untuk merancang abstraksi yang efektif (antarmuka, yaitu) dalam konteks kinerja-kritis sering mengharuskan Anda untuk memahami sebagian besar masalah dan solusi yang sangat efektif untuk itu di muka. Ini benar-benar dapat berubah menjadi ukuran kontra-produktif untuk mencoba menggeneralisasi dan mengabstraksi solusi sambil secara bersamaan mencoba untuk mencari tahu desain yang efektif melalui beberapa iterasi. Salah satu alasannya adalah bahwa konteks kritis kinerja memerlukan representasi data yang sangat efisien dan pola akses. Abstraksi menempatkan penghalang antara kode yang ingin mengakses data: penghalang yang berguna jika Anda ingin data bebas berubah tanpa memengaruhi kode tersebut, tetapi halangan jika Anda secara bersamaan mencoba mencari cara paling efektif untuk mewakili dan akses data tersebut di tempat pertama.
Tetapi jika Anda melakukannya dengan cara ini, sekali lagi saya akan melakukan kesalahan dengan mengubah quadtree menjadi detail implementasi pribadi dari medan Anda, bukan sesuatu untuk digeneralisasi dan digunakan di luar implementasi mereka. Dan Anda harus meninggalkan ide untuk dapat dengan mudah menukar implementasi GPU dari implementasi CPU, karena itu biasanya akan memerlukan datang dengan abstraksi yang bekerja untuk keduanya dan tidak secara langsung tergantung pada detail konkret (seperti repetisi node) dari keduanya.
Point of Decoupling
Tetapi mungkin dalam beberapa kasus ini bahkan dapat diterima untuk hal-hal yang lebih umum digunakan. Sebelum orang-orang berpikir saya mengomel omong kosong, pertimbangkan antarmuka gambar. Berapa banyak dari mereka yang cukup untuk prosesor video yang perlu menerapkan filter gambar pada video secara realtime jika gambar tidak mengekspos internalnya (akses langsung ke array piksel yang mendasarinya dalam format piksel tertentu)? Tidak ada yang saya tahu menggunakan sesuatu seperti abstrak / virtual di getPixel
sini dansetPixel
di sana sambil melakukan konversi format piksel berdasarkan per-piksel. Jadi dalam konteks kinerja-kritis yang cukup di mana Anda harus mengakses hal-hal pada tingkat yang sangat granular (per-pixel, per-node, dll), Anda mungkin kadang-kadang harus mengekspos internal struktur yang mendasarinya. Tetapi akibatnya Anda harus memasangkan beberapa hal dengan erat, dan tidak mudah mengubah representasi gambar yang mendasarinya (misalnya, perubahan dalam format gambar), sehingga tidak mempengaruhi segala sesuatu yang mengakses piksel yang mendasarinya. Tetapi mungkin ada lebih sedikit alasan untuk berubah dalam kasus itu, karena mungkin sebenarnya lebih mudah untuk menstabilkan representasi data daripada antarmuka abstrak. Prosesor video mungkin dapat menerima gagasan menggunakan format piksel RGBA 32-bit dan bahwa keputusan desain mungkin tidak berubah untuk tahun-tahun mendatang.
Idealnya Anda ingin dependensi mengalir menuju stabilitas (hal-hal yang tidak berubah) karena mengubah sesuatu yang memiliki banyak dependensi akan berlipat ganda dalam biaya dengan jumlah dependensi. Itu mungkin atau mungkin bukan abstraksi dalam semua kasus. Tentu saja itu mengabaikan manfaat menyembunyikan informasi dalam mempertahankan invarian, tetapi dari sudut pandang penggandengan, titik utama decoupling adalah untuk membuat hal-hal menjadi lebih murah untuk berubah. Itu berarti mengalihkan ketergantungan dari hal-hal yang dapat berubah menjadi hal-hal yang tidak akan berubah, dan itu tidak membantu sedikit pun jika antarmuka abstrak Anda adalah bagian yang paling cepat berubah dari struktur data Anda.
Jika Anda ingin setidaknya meningkatkan sedikit dari perspektif penggandengan, maka pisahkan bagian simpul Anda yang harus diakses klien dari bagian yang tidak. Saya berasumsi klien setidaknya tidak perlu memperbarui tautan node, jadi tidak perlu membuka tautan. Anda setidaknya harus dapat membuat beberapa nilai agregat yang terpisah dari keseluruhan apa yang diwakili oleh node untuk diakses / dimodifikasi oleh klien NodeValue
.