Przyspieszenie operacji wejścia/wyjścia plików w C/C++, bez dużego wysiłku.

Przyspieszenie operacji wejścia/wyjścia plików w C/C++, bez dużego wysiłku.

Wstęp

Na świecie istnieje takie proste i bardzo przydatne narzędzie — BDelta, i tak wyszło, że od bardzo dawna zakorzeniło się w naszym procesie produkcyjnym (prawda, nie udało się zainstalować jego wersji, ale na pewno nie była to najnowsza dostępna). Używamy jej zgodnie z jej przeznaczeniem — do tworzenia binarnych patchy. Jeśli spojrzeć na repozytorium, staje się nieco smutno: w zasadzie jest ono od dawna porzucone, a wiele w nim jest mocno przestarzałe (kiedyś wprowadził kilka poprawek mój były kolega, ale zdarzyło się to dawno). W każdym razie, postanowiłem ożywić ten projekt: sforkowałem, usunąłem to, czego nie planuję używać, przeniosłem projekt na cmake, zainlinowałem „gorące” mikro-funkcje, usunąłem z stosu duże tablice (i tablice o zmiennym rozmiarze, z których się szczerze „bumuję”), ponownie uruchomiłem profiler — i dowiedziałem się, że około 40% czasu poświęca się na fwrite

Co tam z fwrite?

W tym kodzie fwrite (w moim konkretnym przypadku: tworzenie patcha między podobnymi plikami 300 MB, dane wejściowe w całości w pamięci) wywoływane jest miliony razy z małym rozmiarem bufora. Oczywiście, że ten mechanizm będzie spowalniał, dlatego chciałbym jakoś wpłynąć na ten problem bez wprowadzania różnych źródeł danych, asynchronicznego wejścia/wyjścia na razie nie mam ochoty, chciałem znaleźć prostsze rozwiązanie. Pierwsza myśl, która przyszła mi do głowy — zwiększyć rozmiar bufora

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

ale znaczącej poprawy wyniku nie uzyskałem (teraz fwrite zajmowało około 37% czasu) — więc problem nie leży w częstym zapisywaniu danych na dysku. Zaglądając „pod maskę” fwrite, można zobaczyć, że wewnątrz odbywa się lock/unlock struktury FILE mniej więcej tak (pseudokod, cały analiz przeprowadzono 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);   /* zablokuj strumień */
   __try
   {
      retval = _fwrite_nolock(buffer, size, count, stream);
   }
   __finally 
   {
       _unlock_str(stream);   /* odblokuj strumień */
   }
   return retval;
}

Jeśli wierzyć profilerowi, na _fwrite_nolock przypada tylko 6% czasu, reszta to overhead. W moim przypadku bezpieczeństwo wątkowe to wyraźne nadmiar, więc zdecyduję się je poświęcić, zastępując wywołanie fwrite na _fwrite_nolock — nawet z argumentami nie trzeba kombinować. W rezultacie: ta prosta manipulacja znacznie zmniejszyła koszty zapisu wyniku, które w pierwotnej wersji stanowiły prawie połowę czasu potrzebnego. Przy okazji, w świecie POSIX istnieje podobna funkcja — fwrite_unlocked. Ogólnie rzecz biorąc, to samo dotyczy fread. W ten sposób za pomocą pary #define można uzyskać całkiem uniwersalne rozwiązanie bez zbędnych blokad, jeśli nie są one potrzebne (co zdarza się dość często).

fwrite, _fwrite_nolock, setvbuf

Porzućmy pierwotny projekt i zajmijmy się testowaniem konkretnego przypadku: zapisu dużego pliku (512 MB) w ekstremalnie małych porcjach — po 1 bajcie. System testowy: AMD Ryzen 7 1700, 16 GB RAM, HDD 7200 rpm 64 MB cache, Windows 10 1809, binarka budowana w 32-bitowym, optymalizacje włączone, biblioteka statycznie powiązana.

Przykład do przeprowadzenia eksperymentu:


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

Jako zmienne będą występować TEST_BUFFER_SIZE, a dla pary przypadków zamienimy fwrite_unlocked na fwrite. Zaczniemy od przypadku fwrite bez wyraźnego ustawienia rozmiaru bufora (zakomentujemy setvbuf i powiązany kod): czas 27048906 µs, prędkość zapisu — 18.93 MB/s. Teraz ustawimy rozmiar bufora na 64 KB: czas — 25037111 µs, prędkość — 20.44 MB/s. Teraz przetestujemy działanie _fwrite_nolock bez wywołania setvbuf: 7262221 µs, prędkość — 70.5 MB/s!

Następnie przeprowadzimy eksperymenty z rozmiarem bufora (setvbuf):

Przyspieszenie operacji wejścia/wyjścia plików w C/C++, bez dużego wysiłku.

Dane uzyskano na podstawie średniej z 5 eksperymentów, na obliczanie błędów nie miałem cierpliwości. Z mojego punktu widzenia, 93 MB/s przy zapisie po 1 bajcie na zwykłym HDD — to naprawdę niezły wynik, wystarczy dobrać optymalny rozmiar bufora (w moim przypadku 256 KB — idealnie) i zamienić fwrite na _fwrite_nolock/fwrite_unlocked (jeśli oczywiście nie potrzebna jest bezpieczeństwo wątków).
Podobnie z fread w takich warunkach. Ponieważ nie mam 'metalowej' maszyny z linuxem pod ręką (jednopłytkowce się nie liczą), postanowiłem przeprowadzić ograniczony eksperyment na wirtualnej maszynie (Hyper-V, OpenSUSE 15, GCC 8.3.1) — zasada jest w zasadzie ta sama: 'goły' fwrite 20 MB/s, fwrite + bufor 256 KB dał 23 MB/s, fwrite_unlocked z takim samym buforem — 35 MB/s (binarka 64-bitowa, budowana g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).

Epilog

Celem napisania tego artykułu było opisanie prostego i skutecznego w wielu przypadkach podejścia (z funkcjami _fwrite_nolock/fwrite_unlocked wcześniej się nie spotkałem, nie są zbyt popularne - a szkoda). Nie roszczę sobie prawa do nowości materiału, ale mam nadzieję, że artykuł okaże się przydatny dla społeczności.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster