Accelerarea input/output-ului de fișiere C/C++, fără a te stresa prea mult

Accelerarea input/output-ului de fișiere C/C++, fără a te stresa prea mult

Prefață

Există în lume o unealtă atât de simplă și foarte utilă — BDelta, și astfel s-a întâmplat că s-a integrat foarte bine în procesul nostru de producție (deși nu am reușit să-i instalez versiunea, dar cu siguranță nu era ultima disponibilă). O folosim conform destinației sale — construirea patch-urilor binare. Dacă aruncăm o privire asupra ceea ce este în depozit — devine puțin trist: în esență, a fost abandonat demult și multe acolo sunt deja învechite (cândva, un fost coleg de-al meu a adus câteva modificări, dar a fost acum mult timp). În general, am decis să revigorez acest proiect: m-am forkuit, am eliminat ce nu intenționez să folosesc, am trecut proiectul pe cmake, am inlinat microfuncțiile „calde”, am eliminat din stivă matrici mari (și matrici cu lungime variabilă, care în mod deschis mă „bombardează”), am rulat din nou profilerul — și am aflat că aproximativ 40% din timp este consumat pe fwrite…

Ce este cu fwrite?

În acest cod, fwrite (în cazul meu specific: construirea patch-ului între fișierele apropiate de 300 MB, datele de intrare fiind complet în memorie) este apelat de milioane de ori cu un buffer de dimensiuni mici. Este evident că acest lucru va încetini, așa că aș dori să influențez acest haos fără a implementa diverse tipuri de surse de date sau I/O asincron; voiam să găsesc o soluție mai simplă. Primul lucru care mi-a venit în minte a fost — să cresc dimensiunea buffer-ului

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

dar nu am obținut o îmbunătățire semnificativă a rezultatului (acum fwrite ocupa aproximativ 37% din timp) — așa că problema nu este, de fapt, în scrierea frecventă a datelor pe disc. Dacă ne uităm „sub capotă” la fwrite putem observa că în interior se întâmplă un lock/unlock al structurii FILE cam așa (pseudocod, tot analiza s-a realizat în Visual Studio 2017):


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

Dacă credem profilerului, pe _fwrite_nolock cade doar 6% din timp, restul este overhead. În cazul meu specific, securitatea fluxului este un exces evident, așa că sacrific asta, înlocuind apelul fwrite cu _fwrite_nolock — nici cu argumentele nu trebuie să te complici. Așadar: această manipulare simplă a redus semnificativ costurile pentru salvarea rezultatelor, care în varianta inițială reprezentau aproape jumătate din timpul necesar. Apropo, în lumea POSIX există o funcție similară — fwrite_unlocked. În general vorbind, același lucru se aplică și pentru fread. Astfel, cu ajutorul a câtorva #define putem obține o soluție complet multiplatformă fără blocaje suplimentare, dacă acestea nu sunt necesare (ceea ce se întâmplă destul de des).

fwrite, _fwrite_nolock, setvbuf

Să ne abstragem de la proiectul original și să ne ocupăm de testarea unui caz concret: salvarea unui fișier mare (512 MB) în porții extrem de mici — de 1 byte. Sistemul de testare: AMD Ryzen 7 1700, 16 GB RAM, HDD 7200 rpm 64 MB cache, Windows 10 1809, binarul construit pe 32 de biți, optimizările incluse, biblioteca fiind legată static.

Exemplu pentru desfășurarea experimentului:


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

Ca variabile vor fi folosite TEST_BUFFER_SIZE, iar pentru câteva cazuri vom înlocui fwrite_unlocked cu fwrite. Să începem cu cazul fwrite fără a seta explicit dimensiunea buffer-ului (vom comenta setvbuf și codul asociat): timpul 27048906 μs, viteza de scriere — 18.93 MB/s. Acum vom seta dimensiunea buffer-ului la 64 KB: timpul — 25037111 μs, viteza — 20.44 MB/s. Acum să testăm funcționarea _fwrite_nolock fără a apela setvbuf: 7262221 μs, viteza — 70.5 MB/s!

Mai departe, vom experimenta cu dimensiunea buffer-ului (setvbuf):

Accelerarea input/output-ului de fișiere C/C++, fără a te stresa prea mult

Datele au fost obținute prin medierea a 5 experimente, nu am avut răbdare să contabilizez erorile. După părerea mea, 93 MB/s la scrierea de 1 byte pe un HDD obișnuit este un rezultat foarte bun, trebuie doar să alegi dimensiunea optimă a buffer-ului (în cazul meu 256 KB — perfect) și să înlocuiești fwrite cu _fwrite_nolock/fwrite_unlocked (în cazul în care nu este necesară siguranța de fir, desigur).
Similar cu fread în astfel de condiții. Deoarece nu am la dispoziție o mașină "hardware" cu linux (plăcile unice nu se pun la socoteală), am decis să desfășor un experiment limitat pe o mașină virtuală (Hyper-V, OpenSUSE 15, GCC 8.3.1) — tendința este, în principiu, aceeași: fwrite "goală" 20 MB/s, fwrite + buffer de 256 KB a dat 23 MB/s, fwrite_unlocked cu același buffer — 35 MB/s (binar 64 de biți, construit cu g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).

Cuvânt înainte

Scopul acestui articol a fost descrierea unei metode simple și eficiente, care poate fi aplicată în multe cazuri (funcțiile _fwrite_nolock/fwrite_unlocked nu le am întâlnit anterior, nu sunt foarte populare – și pe bună dreptate). Nu pretind noutatea materialului, dar sper că articolul va fi de folos comunității.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster