Pospešite V/I datoteke C/C++, ne da bi se preznojili

Pospešite V/I datoteke C/C++, ne da bi se preznojili

Predgovor

Na svetu obstaja tako preprost in zelo uporaben pripomoček - BDelta, in zgodilo se je, da je bil že zelo dolgo zasidran v našem proizvodnem procesu (čeprav njegove različice ni bilo mogoče namestiti, vsekakor pa ni bila zadnja na voljo). Uporabljamo ga za predvideni namen - gradnjo binarnih popravkov. Če pogledate, kaj je v repozitoriju, postane malo žalostno: pravzaprav je bilo že zdavnaj opuščeno in veliko je zelo zastarelo (moj nekdanji kolega je tam nekoč večkrat urejal, vendar je bilo to že dolgo nazaj) . Na splošno sem se odločil oživiti to zadevo: razcepil sem se, vrgel ven tisto, česar nisem nameraval uporabiti, projekt premaknil na cmake, vnesel "vroče" mikrofunkcije, odstranil velike nize iz sklada (in nize spremenljive dolžine, zaradi katerih sem odkrito "bomba"), še enkrat zagnal profiler - in ugotovil, da se približno 40 % časa porabi za fwrite...

Kaj je torej s fwrite?

V tej kodi je fwrite (v mojem posebnem testnem primeru: izdelava popravka med tesnimi 300 MB datotekami, vhodni podatki so v celoti v pomnilniku) poklicana milijonkrat z majhno velikostjo medpomnilnika. Očitno se bo ta stvar upočasnila in zato bi rad nekako vplival na to sramoto. Ni še želje po implementaciji različnih vrst podatkovnih virov, asinhronih I/O, želel sem najti enostavnejšo rešitev. Prva stvar, ki mi je prišla na misel, je bila povečati velikost medpomnilnika

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

vendar nisem dosegel bistvenega izboljšanja rezultata (zdaj je fwrite predstavljal približno 37 % časa) - kar pomeni, da še vedno ne gre za pogosto pisanje podatkov na disk. Če pogledate "pod pokrov" fwrite, lahko vidite, da se struktura FILE za zaklepanje/odklepanje dogaja znotraj nečesa takega (psevdo koda, vsa analiza je bila izvedena 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;
}

Po podatkih profilerja _fwrite_nolock predstavlja le 6 % časa, ostalo so režijski stroški. V mojem posebnem primeru je varnost niti očitno pretirana, zato jo bom žrtvoval tako, da bom klic fwrite zamenjal z _fwrite_nolock - sploh ni treba biti pameten z argumenti. Skupaj: ta preprosta manipulacija je znatno zmanjšala stroške beleženja rezultata, ki je v prvotni različici znašal skoraj polovico porabljenega časa. Mimogrede, v svetu POSIX obstaja podobna funkcija - fwrite_unlocked. Na splošno velja enako za fread. Tako lahko z uporabo para #defines dobite popolnoma večplatformsko rešitev brez nepotrebnih zaklepanj, če niso potrebne (in to se zgodi precej pogosto).

fwrite, _fwrite_nolock, setvbuf

Odmaknimo se od prvotnega projekta in se osredotočimo na testiranje specifičnega primera: zapisovanje velike datoteke (512 MB) v izjemno majhnih delih – en bajt na del. Testni sistem: AMD Ryzen 7 1700, 16 GB RAM-a, trdi disk s 7200 vrtljaji na minuto, 64 MB predpomnilnika. Windows 10 1809, binarna datoteka je bila zgrajena kot 32-bitna, optimizacije so omogočene, knjižnica je statično povezana.

Vzorec za poskus:


#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;
}

Spremenljivke bodo TEST_BUFFER_SIZE in v nekaj primerih bomo fwrite_unlocked zamenjali s fwrite. Začnimo s primerom fwrite brez izrecne nastavitve velikosti vmesnega pomnilnika (zakomentirajte setvbuf in povezano kodo): čas 27048906 µs, hitrost zapisovanja - 18.93 MB/s. Zdaj nastavimo velikost medpomnilnika na 64 KB: čas - 25037111 μs, hitrost - 20.44 Mb/s. Zdaj pa preizkusimo delovanje _fwrite_nolock brez klica setvbuf: 7262221 µs, hitrost - 70.5 Mb/s!

Nato poskusimo z velikostjo medpomnilnika (setvbuf):

Pospešite V/I datoteke C/C++, ne da bi se preznojili

Podatki so bili pridobljeni s povprečenjem 5 poskusov; bil sem prelen, da bi izračunal napake. Kar se mene tiče, je 93 MB/s pri zapisovanju 1 bajta na običajni HDD zelo dober rezultat, le izbrati morate optimalno velikost medpomnilnika (v mojem primeru je 256 KB ravno prav) in zamenjati fwrite z _fwrite_nolock/fwrite_unlocked ( seveda v primeru, da varnost niti ni potrebna).
Enako s freadom v podobnih razmerah. Ker pri roki nimam strojne opreme z Linuxom (posamezne plošče ne štejejo), sem se odločil izvesti omejen eksperiment na virtualnem stroju (Hyper-V, OpenSUSE 15, GCC 8.3.1) - vzorec je v bistvu enako: “goli” fwrite 20 Mb/s, fwrite + 256 KB medpomnilnik proizvede 23 Mb/s, fwrite_unlocked z istim medpomnilnikom - 35 Mb/s (64-bitni binarni, sestavljeni g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test. cpp -o fwrite_test).

spremna beseda

Namen pisanja tega članka je bil opisati preprosto in v mnogih primerih učinkovito tehniko (še nikoli nisem naletel na funkcije _fwrite_nolock/fwrite_unlocked, niso zelo priljubljene - a zaman). Ne trdim, da je gradivo novo, vendar upam, da bo članek koristen skupnosti.

Vir: www.habr.com

Kupite zanesljivo gostovanje za strani z DDoS zaščito, VPS VDS strežniki 🔥 Kupite zanesljivo spletno gostovanje z zaščito DDoS, VPS VDS strežniki | ProHoster