Ubrzajte ulaz/izlaz C/C++ datoteka bez brige

Ubrzajte ulaz/izlaz C/C++ datoteka bez brige

Predgovor

Na svijetu postoji tako jednostavan i vrlo koristan uslužni program - BDelta, a dogodilo se da je bio ukorijenjen u našem proizvodnom procesu jako dugo (iako nije bilo moguće instalirati njegovu verziju, ali definitivno nije bila posljednja dostupna). Koristimo ga za njegovu namjenu - izgradnju binarnih zakrpa. Ako pogledate šta se nalazi u spremištu, postaje malo tužno: u stvari, odavno je napušteno i veliki dio je jako zastario (moj bivši kolega je jednom tamo napravio nekoliko izmjena, ali to je bilo davno) . Generalno, odlučio sam da oživim ovu stvar: račvao sam se, izbacio ono što nisam planirao da koristim, preselio projekat na cmake, umetnuo "vruće" mikrofunkcije, uklonio velike nizove iz steka (i nizove promjenjive dužine, što me iskreno čini "bombom"), još jednom pokrenuo profiler - i otkrio da se oko 40% vremena troši na fwrite...

Pa šta je sa fwrite-om?

U ovom kodu, fwrite (u mom specifičnom testnom slučaju: pravljenje zakrpe između blizu 300 MB fajlova, ulazni podaci su u potpunosti u memoriji) se poziva milionima puta sa malom veličinom bafera. Očigledno da će se ovo usporiti, i zato bih želeo da nekako utičem na ovu sramotu. Još nema želje za implementacijom raznih vrsta izvora podataka, asinhronog ulaza-izlaza, htio sam pronaći jednostavnije rješenje. Prvo što mi je palo na pamet je povećanje veličine bafera

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

ali nisam dobio značajno poboljšanje u rezultatu (sada je fwrite činio oko 37% vremena) - što znači da još uvijek nije stvar čestog pisanja podataka na disk. Gledajući “ispod haube” fwrite-a, možete vidjeti da se struktura FILE zaključavanja/otključavanja događa unutar nečega poput ovoga (pseudo-kod, sva analiza je obavljena pod 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;
}

Prema profileru, _fwrite_nolock čini samo 6% vremena, ostalo je prekomjerno. U mom konkretnom slučaju, sigurnost niti je očigledno prevelika, pa ću je žrtvovati zamjenom poziva fwrite sa _fwrite_nolock - ne morate čak ni da budete pametni sa argumentima. Ukupno: ova jednostavna manipulacija značajno je smanjila troškove snimanja rezultata, koji su u originalnoj verziji iznosili skoro polovinu utrošenog vremena. Inače, u POSIX svijetu postoji slična funkcija - fwrite_unlocked. Uopšteno govoreći, isto se odnosi i na fread. Dakle, koristeći par #defines, možete dobiti potpuno višeplatformsko rješenje bez nepotrebnih zaključavanja ako nisu neophodne (a to se događa prilično često).

fwrite, _fwrite_nolock, setvbuf

Udaljimo se od originalnog projekta i fokusirajmo se na testiranje specifičnog slučaja: pisanje velike datoteke (512 MB) u izuzetno malim dijelovima - po jedan bajt. Testni sistem: AMD Ryzen 7 1700, 16 GB RAM-a, 7200 rpm HDD, 64 MB keš memorije. Windows 10 1809, binarna datoteka je izgrađena kao 32-bitna, optimizacije su omogućene, biblioteka je statički povezana.

Uzorak za eksperiment:


#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, 1, sizeof(b), file.get());

    auto end = steady_clock::now();
    auto interval = duration_cast<microseconds>(end - start);
    printf("Time: %lldn", interval.count());

    return 0;
}

Varijable će biti TEST_BUFFER_SIZE, a u nekoliko slučajeva ćemo zamijeniti fwrite_unlocked sa fwrite. Počnimo sa slučajem fwrite bez eksplicitnog postavljanja veličine bafera (komentirajte setvbuf i pridruženi kod): vrijeme 27048906 µs, brzina pisanja - 18.93 MB/s. Sada postavimo veličinu bafera na 64 KB: vrijeme - 25037111 μs, brzina - 20.44 Mb/s. Sada testirajmo rad _fwrite_nolock bez pozivanja setvbuf-a: 7262221 µs, brzina - 70.5 Mb/s!

Zatim, eksperimentirajmo s veličinom bafera (setvbuf):

Ubrzajte ulaz/izlaz C/C++ datoteka bez brige

Podaci su dobijeni u prosjeku 5 eksperimenata; bio sam previše lijen da izračunam greške. Što se mene tiče, 93 MB/s pri pisanju 1 bajta na običan HDD je jako dobar rezultat, samo trebate odabrati optimalnu veličinu bafera (u mom slučaju 256 KB je taman) i zamijeniti fwrite sa _fwrite_nolock/fwrite_unlocked ( u slučaju da sigurnost niti nije potrebna, naravno).
Isto tako i sa fredom u sličnim uslovima. Pošto nemam pri ruci hardversku mašinu sa Linuxom (jednopločni računari se ne računaju), odlučio sam da sprovedem ograničen eksperiment na virtuelnoj mašini (Hyper-V, OpenSUSE 15, GCC 8.3.1) - šablon je, u principu, isti: “goli” fwrite 20 Mb/s, fwrite + 256 KB bafer proizvodi 23 Mb/s, fwrite_unlocked sa istim baferom - 35 Mb/s (64-bitno binarno, sklopljeno g++ -o2 - s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).

Posle reči

Svrha pisanja ovog članka bila je opisati jednostavnu i efikasnu tehniku ​​u mnogim slučajevima (nikad ranije nisam naišao na funkcije _fwrite_nolock/fwrite_unlocked, nisu baš popularne - ali uzalud). Ne pretvaram se da je materijal nov, ali se nadam da će članak biti koristan zajednici.

izvor: www.habr.com

Kupite pouzdan hosting za sajtove sa DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan web hosting sa DDoS zaštitom, VPS VDS servere | ProHoster