Desain Berorientasi Data - tidak praktis dengan lebih dari 1-2 struktur "anggota"?


23

Contoh biasa Desain Berorientasi Data adalah dengan struktur Bola:

struct Ball
{
  float Radius;
  float XYZ[3];
};

dan kemudian mereka membuat beberapa algoritma yang mengulang std::vector<Ball>vektor.

Kemudian mereka memberi Anda hal yang sama, tetapi diimplementasikan dalam Desain Berorientasi Data:

struct Balls
{
  std::vector<float> Radiuses;
  std::vector<XYZ[3]> XYZs;
};

Yang bagus dan semuanya jika Anda akan beralih melalui semua radio terlebih dahulu, lalu semua posisi dan sebagainya. Namun, bagaimana Anda memindahkan bola dalam vektor? Dalam versi aslinya, jika Anda punya std::vector<Ball> BallsAll, Anda bisa memindahkan apa saja BallsAll[x]ke apa saja BallsAll[y].

Namun untuk melakukan itu untuk versi Berorientasi Data, Anda harus melakukan hal yang sama untuk setiap properti (2 kali dalam kasus Ball - radius dan posisi). Tetapi akan menjadi lebih buruk jika Anda memiliki lebih banyak properti. Anda harus menyimpan indeks untuk setiap "bola" dan ketika Anda mencoba untuk memindahkannya, Anda harus melakukan gerakan di setiap vektor properti.

Bukankah itu membunuh manfaat kinerja Desain Berorientasi Data?

Jawaban:


23

Jawaban lain memberikan gambaran yang sangat baik tentang bagaimana Anda merangkum penyimpanan berorientasi baris dan memberikan tampilan yang lebih baik. Tapi karena Anda juga bertanya tentang kinerja, izinkan saya mengatasi itu: Tata letak SoA bukan peluru perak . Ini adalah standar yang cukup bagus (untuk penggunaan cache; tidak begitu banyak untuk kemudahan implementasi di sebagian besar bahasa), tetapi tidak semua ada, bahkan dalam desain berorientasi data (apa pun artinya). Ada kemungkinan bahwa pengarang dari beberapa perkenalan yang Anda baca melewatkan titik itu dan hanya menampilkan tata letak SoA karena mereka pikir itulah inti dari DOD. Mereka salah, dan untungnya tidak semua orang jatuh ke dalam perangkap itu .

Seperti yang mungkin sudah Anda sadari, tidak setiap bagian data primitif diuntungkan dengan ditarik ke dalam susunannya sendiri. Tata letak SoA menguntungkan ketika komponen yang Anda bagi menjadi array yang terpisah biasanya diakses secara terpisah. Tetapi tidak setiap bagian kecil diakses secara terpisah, misalnya vektor posisi hampir selalu dibaca dan diperbarui secara grosir, jadi tentu saja Anda tidak membagi yang itu. Bahkan, teladan Anda juga tidak melakukannya! Demikian juga, jika Anda biasanya mengakses semua properti Ball bersama-sama, karena Anda menghabiskan sebagian besar waktu Anda bertukar bola dalam koleksi bola Anda, tidak ada gunanya memisahkan mereka.

Namun, ada sisi kedua dari DOD. Anda tidak mendapatkan semua kelebihan cache dan organisasi hanya dengan mengubah tata letak memori Anda 90 ° dan melakukan yang paling sedikit untuk memperbaiki kesalahan kompilasi yang dihasilkan. Ada trik umum lainnya yang diajarkan di bawah spanduk ini. Misalnya "pemrosesan berbasis keberadaan": Jika Anda sering menonaktifkan bola dan mengaktifkannya kembali, jangan tambahkan bendera ke objek bola dan buat lingkaran pembaruan abaikan bola dengan bendera yang disetel ke false. Pindahkan bola dari koleksi "aktif" ke koleksi "tidak aktif", dan buat pembaruan hanya memeriksa koleksi "aktif".

Lebih penting dan relevan dengan contoh Anda: Jika Anda menghabiskan begitu banyak waktu mengocok susunan bola, mungkin Anda melakukan sesuatu yang salah. Mengapa pesanan itu penting? Bisakah Anda membuatnya tidak masalah? Jika demikian, Anda akan mendapatkan beberapa manfaat:

  • Anda tidak perlu mengacak koleksi (kode tercepat tidak ada kode sama sekali).
  • Anda dapat menambah dan menghapus lebih mudah dan efisien (bertukar untuk mengakhiri, menjatuhkan yang terakhir).
  • Kode yang tersisa mungkin memenuhi syarat untuk optimasi lebih lanjut (seperti perubahan tata letak yang Anda fokuskan).

Jadi, alih-alih melempar SoA secara membuta pada segala hal, pikirkan tentang data Anda dan bagaimana Anda memprosesnya. Jika Anda menemukan bahwa Anda memproses posisi dan kecepatan dalam satu loop, kemudian pergi melalui jerat, dan kemudian perbarui hitpoints, coba pisahkan tata letak memori Anda menjadi tiga bagian ini. Jika Anda menemukan Anda mengakses komponen x, y, z dari posisi secara terpisah, mungkin mengubah vektor posisi Anda menjadi SoA. Jika Anda mendapati diri Anda mengocok data lebih dari sekadar melakukan sesuatu yang bermanfaat, mungkin berhenti mengocoknya.


18

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 Pixelantarmuka 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).


5

Apa yang Anda uraikan adalah masalah implementasi. Desain OO jelas tidak peduli dengan implementasi.

Anda dapat merangkum wadah Bola berorientasi kolom di belakang antarmuka yang memperlihatkan tampilan berorientasi baris atau kolom. Anda bisa mengimplementasikan objek Bola dengan metode seperti volumedan move, yang hanya memodifikasi nilai masing-masing dalam struktur kolom-bijaksana yang mendasarinya. Pada saat yang sama, wadah Ball Anda dapat mengekspos antarmuka untuk operasi kolom-efisien. Dengan templat / tipe yang sesuai dan kompiler inlining pintar, Anda dapat menggunakan abstraksi ini dengan biaya runtime nol.

Seberapa seringkah Anda mengakses kolom data dan mengubahnya dalam baris? Dalam kasus penggunaan umum untuk penyimpanan kolom, pemesanan baris tidak berpengaruh. Anda bisa mendefinisikan permutasi baris yang sewenang-wenang dengan menambahkan kolom indeks terpisah. Mengubah urutan hanya akan membutuhkan pertukaran nilai di kolom indeks.

Penambahan / penghapusan elemen yang efisien dapat dicapai dengan teknik lain:

  • Pertahankan bitmap dari baris yang dihapus alih-alih menggeser elemen. Padatkan struktur ketika terlalu jarang.
  • Kelompokkan baris menjadi potongan yang sesuai dalam struktur mirip-B sehingga penyisipan atau penghapusan pada posisi sewenang-wenang tidak perlu memodifikasi seluruh struktur.

Kode klien akan melihat urutan objek Ball, wadah objek Ball yang bisa berubah, urutan jari-jari, matriks Nx3, dll; tidak perlu khawatir dengan detail jelek dari struktur yang rumit (tapi efisien) itu. Itulah yang dibeli objek oleh Anda.


+1 Organisasi AoS sangat dapat diubah untuk API berorientasi entitas yang bagus, meskipun diakui itu menjadi lebih buruk untuk digunakan ( ball->do_something();dibandingkan ball_table.do_something(ball)) kecuali jika Anda ingin memalsukan entitas yang koheren melalui pseudo-pointer (&ball_table, index).

1
Saya akan melangkah lebih jauh: Kesimpulan untuk menggunakan SoA dapat dicapai murni dari prinsip-prinsip desain OO. Caranya adalah Anda membutuhkan skenario di mana kolom adalah objek yang lebih mendasar daripada baris. Balls bukan contoh yang baik di sini. Sebagai gantinya, pertimbangkan medan dengan berbagai properti seperti ketinggian, jenis tanah, atau curah hujan. Setiap properti dimodelkan sebagai objek ScalarField, yang memiliki metode sendiri seperti gradien () atau divergensi () yang dapat mengembalikan objek bidang lainnya. Anda dapat merangkum hal-hal seperti resolusi peta, dan properti yang berbeda di medan dapat bekerja dengan resolusi yang berbeda.
16807

4

Jawaban singkat: Anda sepenuhnya benar, dan artikel seperti ini benar-benar kehilangan poin ini.

Jawaban lengkapnya adalah: pendekatan "Structure-Of-Array" dari contoh Anda dapat memiliki keunggulan kinerja untuk beberapa jenis operasi ("operasi kolom"), dan "Array-of-Structs" untuk jenis operasi lain ("operasi baris" ", seperti yang Anda sebutkan di atas). Prinsip yang sama telah memengaruhi arsitektur basis data, ada basis data berorientasi kolom vs. basis data berorientasi baris klasik

Jadi hal kedua yang perlu dipertimbangkan untuk memilih desain adalah jenis operasi apa yang paling Anda butuhkan dalam program Anda, dan jika itu akan mendapat manfaat dari tata letak memori yang berbeda. Namun, hal pertama yang perlu dipertimbangkan adalah jika Anda benar - benar membutuhkan kinerja itu (saya pikir dalam pemrograman game, di mana artikel di atas adalah dari Anda sering memiliki persyaratan ini).

Sebagian besar bahasa OO saat ini menggunakan tata letak memori "Array-Of-Struct" untuk objek dan kelas. Mendapatkan keuntungan dari OO (seperti membuat abstraksi untuk data Anda, enkapsulasi dan lebih banyak ruang lingkup fungsi dasar lokal), biasanya dikaitkan dengan tata letak memori semacam ini. Jadi selama Anda tidak melakukan komputasi kinerja tinggi, saya tidak akan menganggap SoA sebagai pendekatan utama.


3
DOD tidak selalu berarti tata letak Structure-of-Array (SoA). Itu biasa, karena sering cocok dengan pola akses, tetapi ketika tata letak lain berfungsi lebih baik, tentu saja gunakan itu. DOD adalah jauh lebih umum (dan lebih kabur), lebih seperti paradigma desain daripada cara khusus untuk mengeluarkan data. Juga, sementara artikel yang Anda referensikan jauh dari sumber daya terbaik dan memiliki kekurangannya, artikel ini tidak mengiklankan tata letak SoA. Huruf "A" dan "B" dapat ditampilkan secara lengkap Ballsama baiknya dengan mereka dapat berupa individu floatatau vec3s (yang dengan sendirinya akan mengalami transformasi SoA).

2
... dan desain berorientasi baris yang Anda sebutkan selalu dicakup dalam DOD. Ini disebut susunan struktur (AoS), dan perbedaan pada apa yang oleh sebagian besar sumber daya disebut "cara OOP" (untuk yang lebih baik atau lebih buruk) tidak dalam tata letak baris vs. kolom, melainkan hanya bagaimana tata letak ini dipetakan ke memori (banyak objek kecil terhubung melalui pointer versus tabel kontinu besar semua catatan). Singkatnya, -1 karena meskipun Anda menaikkan poin bagus terhadap kesalahpahaman OP, Anda salah menggambarkan seluruh jazz DOD daripada mengoreksi pemahaman OP tentang DOD.

@delnan: terima kasih atas komentar Anda, Anda mungkin benar bahwa saya seharusnya menggunakan istilah "SoA" daripada "DOD". Saya mengedit jawaban saya sesuai dengan itu.
Doc Brown

Jauh lebih baik, unduhan dihapus. Lihatlah jawaban user2313838 untuk bagaimana SoA dapat disatukan dengan API berorientasi objek "bagus" (dalam arti abstraksi, enkapsulasi, dan "lebih banyak cakupan lokal fungsi-fungsi dasar"). Itu datang lebih alami untuk tata letak AoS (karena array bisa menjadi wadah generik bodoh daripada menikah dengan jenis elemen) tapi itu layak.

Dan ini github.com/BSVino/JaiPrimer/blob/master/JaiPrimer.md yang memiliki konversi otomatis dari SoA ke / dari AoS Contoh: reddit.com/r/rust/comments/2t6xqz/… dan kemudian ada ini: berita. ycombinator.com/item?id=10235766
Jerry Jeremiah
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.