Pola Pikir Berorientasi Data
Desain berorientasi data tidak berarti menerapkan SoA di mana saja. Ini hanya berarti merancang arsitektur dengan fokus utama pada representasi data - khususnya dengan fokus pada tata letak memori yang efisien dan akses memori.
Itu mungkin dapat menyebabkan reps SoA bila sesuai seperti:
struct BallSoa
{
vector<float> x; // size n
vector<float> y; // size n
vector<float> z; // size n
vector<float> r; // size n
};
... ini sering cocok untuk logika loopy vertikal yang tidak memproses komponen vektor pusat lingkaran dan jari-jari secara bersamaan (empat bidang tidak serentak panas), tetapi sebagai gantinya satu per satu (satu loop melalui jari-jari, 3 loop lainnya) melalui komponen individual pusat bola).
Dalam kasus lain mungkin lebih tepat untuk menggunakan AoS jika bidang sering diakses bersama-sama (jika logika gila Anda iterasi melalui semua bidang bola daripada secara individual) dan / atau jika diperlukan akses bola secara acak:
struct BallAoS
{
float x;
float y;
float z;
float r;
};
vector<BallAoS> balls; // size n
... dalam kasus lain, mungkin cocok menggunakan hibrida yang menyeimbangkan kedua manfaat:
struct BallAoSoA
{
float x[8];
float y[8];
float z[8];
float r[8];
};
vector<BallAoSoA> balls; // size n/8
... Anda bahkan dapat mengompresi ukuran bola menjadi setengah menggunakan setengah mengapung agar sesuai dengan lebih banyak bidang bola ke dalam baris / halaman cache.
struct BallAoSoA16
{
Float16 x2[16];
Float16 y2[16];
Float16 z2[16];
Float16 r2[16];
};
vector<BallAoSoA16> balls; // size n/16
... mungkin bahkan jari-jari tidak diakses hampir sesering pusat bola (mungkin basis kode Anda sering memperlakukannya seperti titik dan hanya jarang sebagai bola, misalnya). Dalam hal ini, Anda dapat menerapkan teknik pemisahan bidang panas / dingin lebih lanjut.
struct BallAoSoA16Hot
{
Float16 x2[16];
Float16 y2[16];
Float16 z2[16];
};
vector<BallAoSoA16Hot> balls; // size n/16: hot fields
vector<Float16> ball_radiuses; // size n: cold fields
Kunci dari desain berorientasi data adalah mempertimbangkan semua jenis representasi ini sejak awal dalam membuat keputusan desain Anda, untuk tidak menjebak diri Anda ke dalam representasi yang kurang optimal dengan antarmuka publik di belakangnya.
Ini menyoroti pola akses memori dan tata letak yang menyertainya, menjadikannya perhatian yang jauh lebih kuat dari biasanya. Dalam beberapa hal bahkan mungkin meruntuhkan abstraksi. Saya telah menemukan dengan menerapkan pola pikir ini lebih banyak yang saya tidak lagi melihat std::deque
, misalnya, dalam hal persyaratan algoritmiknya sebanyak representasi blok berdekatan agregat yang dimilikinya dan bagaimana akses acak itu bekerja pada tingkat memori. Ini agak menempatkan fokus pada detail implementasi, tetapi detail implementasi yang cenderung memiliki dampak yang sama banyak atau lebih pada kinerja dengan kompleksitas algoritmik yang menggambarkan skalabilitas.
Optimalisasi Dini
Banyak fokus utama dari desain berorientasi data akan muncul, setidaknya secara sekilas, sangat dekat dengan optimasi prematur. Pengalaman sering mengajarkan kita bahwa optimasi mikro seperti itu paling baik diterapkan di belakang, dan dengan profiler di tangan.
Namun mungkin pesan yang kuat untuk diambil dari desain berorientasi data adalah memberikan ruang untuk optimasi tersebut. Itulah yang dapat dibantu oleh pola pikir berorientasi data:
Desain berorientasi data dapat meninggalkan ruang bernapas untuk mengeksplorasi representasi yang lebih efektif. Ini tidak selalu tentang mencapai kesempurnaan tata letak memori dalam satu perjalanan, tetapi lebih lanjut tentang membuat pertimbangan yang tepat sebelumnya untuk memungkinkan representasi yang semakin optimal.
Desain Berorientasi Objek Granular
Banyak diskusi desain berorientasi data akan mengadu domba konsep klasik pemrograman berorientasi objek. Namun saya akan menawarkan cara memandang hal ini yang tidak sesulit untuk mengabaikan OOP sepenuhnya.
Kesulitan dengan desain berorientasi objek adalah bahwa hal itu akan sering menggoda kita untuk memodelkan antarmuka pada tingkat yang sangat granular, membuat kita terjebak dengan skalar, pola pikir satu per satu, bukan pola pikir massal paralel.
Sebagai contoh berlebihan, bayangkan pola pikir desain berorientasi objek yang diterapkan pada satu piksel gambar.
class Pixel
{
public:
// Pixel operations to blend, multiply, add, blur, etc.
private:
Image* image; // back pointer to access adjacent pixels
unsigned char rgba[4];
};
Semoga tidak ada yang benar-benar melakukan ini. Untuk membuat contoh ini benar-benar kotor, saya menyimpan pointer kembali ke gambar yang mengandung piksel sehingga dapat mengakses piksel tetangga untuk algoritma pemrosesan gambar seperti blur.
Pointer kembali gambar segera menambahkan overhead mencolok, tetapi bahkan jika kita mengecualikannya (membuat antarmuka publik pixel menyediakan operasi yang berlaku untuk satu piksel), kita berakhir dengan kelas hanya untuk mewakili piksel.
Sekarang tidak ada yang salah dengan kelas dalam arti overhead langsung dalam konteks C ++ selain pointer belakang ini. Mengoptimalkan kompiler C ++ sangat bagus dalam mengambil semua struktur yang kami bangun dan melenyapkannya menjadi berkeping-keping.
Kesulitannya di sini adalah kita memodelkan antarmuka yang dienkapsulasi pada tingkat piksel terlalu terperinci. Yang membuat kami terjebak dengan jenis desain dan data granular ini, dengan potensi besar ketergantungan klien yang menyatukan mereka ke Pixel
antarmuka ini .
Solusi: lenyapkan struktur berorientasi objek dari piksel granular, dan mulailah memodelkan antarmuka Anda pada tingkat yang lebih kasar dengan menangani sejumlah besar piksel (pada tingkat gambar).
Dengan memodelkan pada tingkat gambar massal, kami memiliki lebih banyak ruang untuk dioptimalkan. Kita dapat, misalnya, mewakili gambar besar sebagai ubin gabungan 16x16 piksel yang sangat cocok dengan garis cache 64-byte tetapi memungkinkan akses vertikal tetangga yang efisien piksel dengan langkah-langkah yang biasanya kecil (jika kita memiliki sejumlah algoritma pemrosesan gambar yang perlu mengakses piksel tetangga secara vertikal) sebagai contoh berorientasi data hardcore.
Mendesain pada Tingkat yang Lebih Kasar
Contoh antarmuka pemodelan di atas pada tingkat gambar adalah contoh yang tidak perlu digali karena pemrosesan gambar adalah bidang yang sangat matang yang telah dipelajari dan dioptimalkan hingga mati. Namun yang kurang jelas mungkin adalah partikel dalam penghasil partikel, sprite vs kumpulan sprite, tepi pada grafik tepi, atau bahkan seseorang vs kumpulan orang.
Kunci untuk memungkinkan optimasi berorientasi data (dalam tinjauan ke masa depan atau belakang) sering kali akan mengarah pada merancang antarmuka pada tingkat yang jauh lebih kasar, dalam jumlah besar. Gagasan mendesain antarmuka untuk entitas tunggal digantikan dengan mendesain untuk koleksi entitas dengan operasi besar yang memprosesnya secara massal. Ini terutama dan segera menargetkan loop akses berurutan yang perlu mengakses semuanya dan tidak bisa tidak memiliki kompleksitas linier.
Desain berorientasi data sering dimulai dengan ide penggabungan data untuk membentuk data pemodelan agregat secara massal. Pola pikir yang serupa bergema dengan desain antarmuka yang menyertainya.
Ini adalah pelajaran paling berharga yang saya ambil dari desain berorientasi data, karena saya tidak cukup paham arsitektur komputer untuk sering menemukan tata letak memori paling optimal untuk sesuatu pada percobaan pertama saya. Ini menjadi sesuatu yang saya lakukan dengan profiler di tangan (dan kadang-kadang dengan beberapa kesalahan di mana saya gagal mempercepat). Namun aspek desain antarmuka dari desain berorientasi data adalah apa yang membuat saya ruang untuk mencari representasi data yang lebih dan lebih efisien.
Kuncinya adalah merancang antarmuka pada tingkat yang lebih kasar daripada yang biasanya kita tergoda untuk melakukannya. Ini juga sering memiliki manfaat samping seperti mengurangi overhead pengiriman dinamis yang terkait dengan fungsi virtual, panggilan fungsi pointer, panggilan dylib dan ketidakmampuan bagi mereka untuk diuraikan. Gagasan utama untuk menghilangkan semua ini adalah untuk melihat pemrosesan secara massal (jika berlaku).
ball->do_something();
dibandingkanball_table.do_something(ball)
) kecuali jika Anda ingin memalsukan entitas yang koheren melalui pseudo-pointer(&ball_table, index)
.