Mempercepat C / C ++ file I / O tanpa benar-benar melelahkan

gambar

Kata Pengantar


Ada utilitas yang sangat sederhana dan sangat berguna di dunia - BDelta , dan kebetulan itu mengakar dalam proses produksi kami untuk waktu yang lama (meskipun versinya tidak dapat diinstal, tetapi tentu saja itu bukan yang terakhir tersedia). Kami menggunakannya untuk tujuan yang dimaksudkan - pembangunan tambalan biner. Jika Anda melihat apa yang ada di dalam repositori, itu menjadi sedikit sedih: pada kenyataannya, itu sudah lama ditinggalkan dan banyak yang sudah sangat ketinggalan zaman di sana (begitu mantan kolega saya membuat beberapa koreksi di sana, tapi itu sudah lama sekali). Secara umum, saya memutuskan untuk menghidupkan kembali bisnis ini: saya bercabang, membuang apa yang tidak saya rencanakan untuk digunakan, menyalip proyek pada cmake , segariskan mikrofungsi โ€œpanasโ€, menghapus array besar dari tumpukan (dan array panjang variabel, dari mana saya secara terbuka โ€œmembomโ€) , sekali lagi menggerakkan profiler - dan menemukan bahwa sekitar 40% dari waktu dihabiskan untuk menulis ...

Jadi ada apa dengan fwrite?


Dalam kode ini, fwrite (dalam kasus pengujian khusus saya: membangun tambalan antara 300 file MB yang dekat, data input sepenuhnya dalam memori) disebut jutaan kali dengan buffer kecil. Jelas, hal ini akan melambat, dan karena itu saya ingin mempengaruhi aib ini. Tidak ada keinginan untuk mengimplementasikan berbagai jenis sumber data, asinkron input-output, saya ingin mencari solusi yang lebih mudah. Hal pertama yang terlintas dalam pikiran adalah menambah ukuran buffer

setvbuf(file, nullptr, _IOFBF, 64* 1024) 

tapi saya tidak mendapatkan peningkatan yang signifikan dalam hasilnya (sekarang fwrite menyumbang sekitar 37% dari waktu) - itu berarti masalah ini masih belum sering dalam perekaman data ke disk. Melihat fwrite "di bawah tenda", Anda dapat melihat bahwa struktur FILE mengunci / membuka terjadi di dalam seperti ini (kode semu, semua analisis dilakukan di bawah Visual Studio 2017):

 size_t fwrite (const void *buffer, size_t size, size_t count, FILE *stream) { size_t retval = 0; _lock_str(stream); /* lock stream */ __try { retval = _fwrite_nolock(buffer, size, count, stream); } __finally { _unlock_str(stream); /* unlock stream */ } return retval; } 

Menurut profiler, akun _fwrite_nolock hanya 6% dari waktu, sisanya adalah overhead. Dalam kasus khusus saya, keamanan utas jelas merupakan kelebihan, saya akan mengorbankannya dengan mengganti panggilan fwrite dengan _fwrite_nolock - bahkan dengan argumen saya tidak perlu pintar . Total: manipulasi sederhana ini kadang-kadang mengurangi biaya untuk merekam hasil, yang dalam versi aslinya berjumlah hampir setengah dari biaya waktu. By the way, di dunia POSIX ada fungsi yang sama - fwrite_unlocked . Secara umum, hal yang sama berlaku untuk ketakutan. Jadi, menggunakan pasangan #define, Anda bisa mendapatkan sendiri solusi lintas-platform tanpa kunci yang tidak perlu jika tidak diperlukan (dan ini cukup sering terjadi).

fwrite, _fwrite_nolock, setvbuf


Mari abstrak dari proyek asli dan mulai menguji kasus tertentu: merekam file besar (512 MB) dalam porsi yang sangat kecil - 1 byte. Sistem uji: AMD Ryzen 7 1700, RAM 16 GB, HDD 3,5 "7200 rpm 64 MB cache, Windows 10 1809, binar dibangun dalam 32-bit, termasuk optimasi, perpustakaan terhubung secara statis.

Sampel untuk percobaan:

 #include <chrono> #include <cstdio> #include <inttypes.h> #include <memory> #ifdef _MSC_VER #define fwrite_unlocked _fwrite_nolock #endif using namespace std::chrono; int main() { std::unique_ptr<FILE, int(*)(FILE*)> file(fopen("test.bin", "wb"), fclose); if (!file) return 1; constexpr size_t TEST_BUFFER_SIZE = 256 * 1024; if (setvbuf(file.get(), nullptr, _IOFBF, TEST_BUFFER_SIZE) != 0) return 2; auto start = steady_clock::now(); const uint8_t b = 77; constexpr size_t TEST_FILE_SIZE = 512 * 1024 * 1024; for (size_t i = 0; i < TEST_FILE_SIZE; ++i) fwrite_unlocked(&b, sizeof(b), 1, file.get()); auto end = steady_clock::now(); auto interval = duration_cast<microseconds>(end - start); printf("Time: %lld\n", interval.count()); return 0; } 

Variabelnya adalah TEST_BUFFER_SIZE, dan untuk beberapa kasus kami akan mengganti fwrite_unlocked dengan fwrite. Mari kita mulai dengan case fwrite tanpa secara eksplisit mengatur ukuran buffer (komentar keluar setvbuf dan kode yang terkait): waktu 27048906 ฮผs, kecepatan tulis - 18,93 Mb / s. Sekarang atur ukuran buffer ke 64 Kb: waktu - 25037111 ฮผs, kecepatan - 20,44 Mb / s. Sekarang kami menguji operasi _fwrite_nolock tanpa memanggil setvbuf: 7262221 ms, kecepatannya 70,5 Mb / s!

Selanjutnya, bereksperimen dengan ukuran buffer (setvbuf):



Data diperoleh dengan rata-rata 5 percobaan, saya terlalu malas untuk mempertimbangkan kesalahan. Bagi saya, 93 MB / s saat menulis 1 byte ke HDD biasa adalah hasil yang sangat baik, cukup pilih ukuran buffer optimal (dalam kasus saya 256 KB - tepat) dan ganti fwrite dengan _fwrite_nolock / fwrite_unlocked (dalam jika keamanan benang tidak diperlukan, tentu saja).
Begitu pula dengan ketakutan dalam kondisi serupa. Sekarang mari kita lihat bagaimana keadaan di Linux, konfigurasi pengujian adalah sebagai berikut: AMD Ryzen 7 1700X, RAM 16 GB, HDD 3.5 "7200 rpm 64 cache cache, OS OpenSUSE 15, GCC 8.3.1, kami akan menguji x86-64 binar, sistem file pada bagian tes ext4 Hasil fwrite tanpa secara eksplisit mengatur ukuran buffer dalam tes ini adalah 67,6 Mb / s, ketika mengatur buffer ke 256 Kb kecepatan meningkat menjadi 69,7 Mb / s. Sekarang kita akan melakukan pengukuran yang sama untuk fwrite_unlocked - hasilnya masing-masing adalah 93,5 dan 94,6 Mb / s. Memvariasikan ukuran buffer dari 1 KB hingga 8 MB membawa saya ke kesimpulan berikut: meningkatkan buffer meningkatkan kecepatan tulis, tetapi perbedaan dalam kasus saya hanya 3 Mb / s, saya tidak melihat perbedaan kecepatan antara buffer 64 Kb dan 8 Mb sama sekali. Dari data yang diterima pada mesin Linux ini, kami dapat menarik kesimpulan sebagai berikut:

  • fwrite_unlocked lebih cepat dari fwrite, tetapi perbedaan dalam kecepatan tulis tidak sebesar di Windows
  • Ukuran buffer di Linux tidak memiliki pengaruh yang signifikan terhadap kecepatan tulis melalui fwrite / fwrite_unlocked seperti pada Windows


Secara total, metode yang diusulkan efektif baik pada Windows, tetapi juga di Linux (meskipun pada tingkat yang jauh lebih rendah).

Kata penutup


Tujuan artikel ini adalah untuk menggambarkan teknik sederhana dan efektif dalam banyak kasus (saya tidak menemukan fungsi _fwrite_nolock / fwrite_unlocked sebelumnya, mereka tidak sangat populer - tetapi sia-sia). Saya tidak berpura-pura sebagai orang baru dalam materi ini, tetapi saya berharap artikel itu akan bermanfaat bagi masyarakat.

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


All Articles