OutOfLine - pola dalam-memori untuk aplikasi C ++ kinerja tinggi

Saat bekerja di Headlands Technologies, saya cukup beruntung untuk menulis beberapa utilitas untuk menyederhanakan pembuatan kode C ++ berkinerja tinggi. Artikel ini menawarkan gambaran umum tentang salah satu dari utilitas ini, OutOfLine .


Mari kita mulai dengan contoh ilustratif. Misalkan Anda memiliki sistem yang berhubungan dengan sejumlah besar objek sistem file. Ini bisa berupa file biasa, bernama soket atau pipa UNIX. Untuk beberapa alasan, Anda membuka banyak deskriptor file saat startup, kemudian bekerja secara intensif dengan mereka, dan pada akhirnya, tutup deskriptor dan hapus tautan ke file (kira-kira. Jalur berarti fungsi hapus tautan ).


Versi awal (disederhanakan) mungkin terlihat seperti ini:


 class UnlinkingFD { std::string path; public: int fd; UnlinkingFD(const std::string& p) : path(p) { fd = open(p.c_str(), O_RDWR, 0); } ~UnlinkingFD() { close(fd); unlink(path.c_str()); } UnlinkingFD(const UnlinkingFD&) = delete; }; 

Dan ini adalah desain yang bagus dan logis. Itu bergantung pada RAII untuk secara otomatis melepaskan deskriptor dan menghapus tautan. Anda bisa membuat array besar dari objek-objek seperti itu, bekerja dengannya, dan ketika array tidak ada lagi, objek-objek itu sendiri akan menghapus semua yang diperlukan dalam proses.


Tetapi bagaimana dengan kinerja? Misalkan fd digunakan sangat sering, dan path hanya saat menghapus suatu objek. Sekarang array terdiri dari objek berukuran 40 byte, tetapi seringkali hanya 4 byte yang digunakan. Ini berarti bahwa akan ada lebih banyak kesalahan dalam cache, karena Anda perlu "melewati" 90% data.


Salah satu solusi umum untuk masalah ini adalah transisi dari array struktur ke struktur array. Ini akan memberikan kinerja yang diinginkan, tetapi dengan mengorbankan RAII. Apakah ada opsi yang menggabungkan keunggulan dari kedua pendekatan?


Kompromi sederhana adalah mengganti std::string ukuran 32 byte dengan std::unique_ptr<std::string> , yang ukurannya hanya 8 byte. Ini akan mengurangi ukuran objek kita dari 40 byte menjadi 16 byte, yang merupakan pencapaian besar. Tetapi solusi ini masih kalah jika menggunakan banyak array.


OutOfLine adalah alat yang memungkinkan tanpa meninggalkan RAII untuk sepenuhnya memindahkan bidang (dingin) yang jarang digunakan di luar objek. OutOfLine digunakan sebagai kelas dasar CRTP , jadi argumen pertama ke template harus kelas anak. Argumen kedua adalah tipe data yang jarang digunakan (dingin) yang dikaitkan dengan objek (utama) yang sering digunakan.


 struct UnlinkingFD : private OutOfLine<UnlinkingFD, std::string> { int fd; UnlinkingFD(const std::string& p) : OutOfLine<UnlinkingFD, std::string>(p) { fd = open(p.c_str(), O_RDWR, 0); } ~UnlinkingFD(); UnlinkingFD(const UnlinkingFD&) = delete; }; 

Jadi seperti apa kelas ini?


 template <class FastData, class ColdData> class OutOfLine { 

Gagasan implementasi dasar adalah menggunakan wadah asosiatif global yang memetakan pointer ke objek utama dan pointer ke objek yang berisi data dingin.


  inline static std::map<OutOfLine const*, std::unique_ptr<ColdData>> global_map_; 

OutOfLine dapat digunakan dengan semua jenis data dingin, sebuah instance yang dibuat dan dikaitkan dengan objek utama secara otomatis.


  template <class... TArgs> explicit OutOfLine(TArgs&&... args) { global_map_[this] = std::make_unique<ColdData>(std::forward<TArgs>(args)...); } 

Menghapus objek utama memerlukan penghapusan otomatis objek dingin terkait:


  ~OutOfLine() { global_map_.erase(this); } 

Saat memindahkan (memindahkan konstruktor / memindahkan tugas operator) dari objek utama, objek dingin yang sesuai akan secara otomatis dikaitkan dengan objek penerus utama baru. Akibatnya, Anda tidak boleh mengakses data dingin objek yang dipindahkan dari.


  explicit OutOfLine(OutOfLine&& other) { *this = other; } OutOfLine& operator=(OutOfLine&& other) { global_map_[this] = std::move(global_map_[&other]); return *this; } 

Dalam contoh implementasi di atas , OutOfLine dibuat tidak dapat disalin untuk kesederhanaan. Jika perlu, operasi penyalinan mudah ditambahkan, mereka hanya perlu membuat dan menautkan salinan objek dingin.


 OutOfLine(OutOfLine const&) = delete; OutOfLine& operator=(OutOfLine const&) = delete; 

Nah, agar ini benar-benar bermanfaat, alangkah baiknya memiliki akses ke data dingin. Ketika mewarisi dari OutOfLine kelas menerima metode cold() konstan dan tidak konstan cold() :


  ColdData& cold() noexcept { return *global_map_[this]; } ColdData const& cold() const noexcept { return *global_map_[this]; } 

Mereka mengembalikan tipe referensi yang sesuai ke data dingin.


Itu hampir semuanya. Opsi UnlinkingFD ini akan berukuran 4 byte, menyediakan akses yang mudah digunakan untuk cache ke bidang fd , dan mempertahankan manfaat RAII. Semua pekerjaan yang berkaitan dengan siklus hidup suatu objek sepenuhnya otomatis. Ketika objek utama yang sering digunakan bergerak, jarang digunakan data dingin bergerak bersamanya. Ketika objek utama dihapus, objek dingin yang sesuai juga dihapus.


Namun, kadang-kadang, data Anda berkonspirasi untuk menyulitkan hidup Anda - dan Anda dihadapkan pada situasi di mana data dasar harus dibuat terlebih dahulu. Sebagai contoh, mereka diperlukan untuk membangun data dingin. Ada kebutuhan untuk membuat objek dalam urutan terbalik relatif terhadap apa yang ditawarkan OutOfLine . Untuk kasus seperti itu, "cadangan" berguna bagi kita untuk mengontrol urutan inisialisasi dan de-inisialisasi.


  struct TwoPhaseInit {}; OutOfLine(TwoPhaseInit){} template <class... TArgs> void init_cold_data(TArgs&&... args) { global_map_.find(this)->second = std::make_unique<ColdData>(std::forward<TArgs>(args)...); } void release_cold_data() { global_map_[this].reset(); } 

Ini adalah konstruktor OutOfLine lain yang dapat digunakan di kelas anak-anak, ia menerima tag tipe TwoPhaseInit . Jika Anda membuat OutOfLine dengan cara ini, data dingin tidak akan diinisialisasi, dan objek akan tetap setengah dibangun. Untuk menyelesaikan konstruksi dua fase, Anda perlu memanggil metode init_cold_data (menyampaikan argumen yang diperlukan untuk membuat objek tipe ColdData ). Ingat bahwa Anda tidak dapat memanggil .cold() pada objek yang datanya dingin belum diinisialisasi. Dengan analogi, data dingin dapat dihapus lebih cepat dari jadwal sebelum mengeksekusi ~OutOfLine destructor dengan memanggil release_cold_data .


 }; // end of class OutOfLine 

Sekarang semuanya. Jadi apa yang diberikan 29 baris kode ini kepada kita? Mereka adalah tradeoff lain yang mungkin antara kinerja dan kemudahan penggunaan. Dalam kasus di mana Anda memiliki objek, beberapa yang anggotanya digunakan lebih sering daripada yang lain, OutOfLine dapat berfungsi sebagai cara yang mudah digunakan untuk mengoptimalkan cache, dengan biaya secara signifikan memperlambat akses ke data yang jarang digunakan.


Kami dapat menerapkan teknik ini di beberapa tempat - cukup sering ada kebutuhan untuk melengkapi data kerja yang digunakan secara intensif dengan metadata tambahan yang diperlukan di akhir pekerjaan, dalam situasi yang jarang atau tidak terduga. Baik itu informasi tentang pengguna yang membuat koneksi, dari terminal perdagangan dari mana pesanan datang, atau gagang akselerator perangkat keras yang terlibat dalam pemrosesan data pertukaran - OutOfLine menjaga cache tetap bersih ketika Anda berada di bagian penting dari perhitungan (jalur kritis).


Saya menyiapkan tes sehingga Anda dapat melihat dan mengevaluasi perbedaannya.


SkripWaktu
Data dingin di objek utama (versi awal)34684547
Data dingin sepenuhnya dihapus (skenario kasus terbaik)2938327
Menggunakan OutOfLine2947645

Saya mendapat akselerasi OutOfLine saat menggunakan OutOfLine . Jelas, tes ini dirancang untuk menunjukkan potensi OutOfLine , tetapi juga menunjukkan berapa banyak optimasi cache dapat memiliki dampak signifikan pada kinerja, seperti halnya OutOfLine memungkinkan OutOfLine untuk mendapatkan optimasi ini. Menjaga cache tetap bebas dari data yang jarang digunakan dapat memberikan perbaikan yang kompleks, terukur, komprehensif untuk sisa kode. Seperti biasa dengan optimasi, pengukuran kepercayaan lebih dari asumsi, namun saya harap OutOfLine akan terbukti menjadi alat yang berguna dalam koleksi utilitas Anda.


Catatan dari penerjemah


Kode yang disediakan dalam artikel berfungsi untuk menunjukkan gagasan dan tidak mewakili kode produksi.

Source: https://habr.com/ru/post/id421475/


All Articles