Accelerera C/C++ fil I/O utan att svettas

Accelerera C/C++ fil I/O utan att svettas

Förord

Det finns ett sĂ„ enkelt och mycket anvĂ€ndbart verktyg i vĂ€rlden - BDelta, och det rĂ„kade vara sĂ„ att den hade varit en del av vĂ„r produktionsprocess under mycket lĂ„ng tid (Ă€ven om vi inte kunde faststĂ€lla dess version, var den verkligen inte den senaste tillgĂ€ngliga). Vi anvĂ€nder det för sitt avsedda syfte – att bygga binĂ€ra patchar. Om man tittar pĂ„ vad som finns i arkivet blir det lite sorgligt: ​​faktum Ă€r att det övergavs för lĂ€nge sedan och mycket av det Ă€r vĂ€ldigt förĂ„ldrat (min tidigare kollega gjorde en gĂ„ng flera redigeringar dĂ€r, men det var lĂ€nge sedan). I allmĂ€nhet bestĂ€mde jag mig för att Ă„teruppliva den hĂ€r saken: Jag gafflade, slĂ€ngde ut det jag inte planerar att anvĂ€nda och flyttade projektet till cmmake, inlineade "heta" mikrofunktioner, tog bort stora arrayer frĂ„n stacken (och arrayer med variabel lĂ€ngd, vilket Ă€rligt talat "pumpade" mig), körde profileraren igen - och upptĂ€ckte att ungefĂ€r 40 % av tiden Ă€gnas Ă„t fwrite.

SÄ vad Àr det med fwrite?

I den hÀr koden anropas fwrite (i mitt specifika testfall: att bygga en patch mellan tÀtt placerade 300 MB-filer, indata helt i minnet) miljontals gÄnger med en liten buffert. Det Àr uppenbart att det hÀr kommer att sakta ner, och dÀrför skulle jag vilja pÄverka denna skam pÄ nÄgot sÀtt. Det finns ingen önskan att implementera olika typer av datakÀllor eller asynkron input/output Ànnu; Jag skulle vilja hitta en enklare lösning. Det första jag tÀnkte pÄ var att öka buffertstorleken.

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

men jag fick ingen signifikant förbĂ€ttring av resultatet (nu stod fwrite för cirka 37 % av tiden) – sĂ„ problemet ligger inte i frekvent skrivning av data till disk. Om man tittar "under huven" pĂ„ fwrite kan man se att lĂ„sning/upplĂ„sning av FILE-strukturer sker inuti nĂ„got liknande detta (pseudokod, all analys utfördes under 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;
}

Enligt profileraren stĂ„r _fwrite_nolock endast för 6 % av tiden, resten Ă€r omkostnader. I mitt specifika fall Ă€r trĂ„dsĂ€kerhet helt klart redundant, sĂ„ jag kommer att offra den genom att ersĂ€tta fwrite-anropet med _fwrite_nolock — det finns ingen anledning att ens vara smart med argument. Totalt sett minskade denna enkla manipulation kostnaderna för att registrera resultatet flera gĂ„nger, vilket i originalversionen stod för nĂ€stan hĂ€lften av den tidsĂ„tgĂ„ng som anvĂ€ndes. Förresten, det finns en liknande funktion i POSIX-vĂ€rlden - fwrite_unlocked. Generellt sett gĂ€ller detsamma för fread. SĂ„ledes, med hjĂ€lp av ett par #define s, kan du fĂ„ en helt plattformsoberoende lösning utan onödiga lĂ„s ifall de inte behövs (och detta hĂ€nder ganska ofta).

fwrite, _fwrite_nolock, setvbuf

LĂ„t oss gĂ„ ifrĂ„n det ursprungliga projektet och fokusera pĂ„ att testa ett specifikt fall: att skriva en stor fil (512 MB) i extremt smĂ„ bitar – en byte vardera. Testsystem: AMD Ryzen 7 1700, 16 GB RAM, 7200 rpm hĂ„rddisk, 64 MB cache. Windows 10 1809, binĂ€rfilen byggdes som 32-bitars, optimeringar Ă€r aktiverade och biblioteket Ă€r statiskt lĂ€nkat.

Prov för experimentet:


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

Variablerna kommer att vara TEST_BUFFER_SIZE, och i ett par fall kommer vi att ersĂ€tta fwrite_unlocked med fwrite. LĂ„t oss börja med fwrite-fallet utan att explicit ange buffertstorleken (kommentera ut setvbuf och tillhörande kod): tid 27048906 ”s, skrivhastighet 18.93 MB/s. Nu stĂ€ller vi in ​​buffertstorleken till 64 KB: tid - 25037111 ”s, hastighet - 20.44 Mbps. Nu ska vi testa funktionen hos _fwrite_nolock utan att anropa setvbuf: 7262221 ”s, hastighet - 70.5 Mb/s!

Nu ska vi experimentera med buffertstorleken (setvbuf):

Accelerera C/C++ fil I/O utan att svettas

Data erhölls genom att medelvÀrdet berÀknades för 5 experiment; Jag var för lat för att berÀkna felen. Enligt min mening Àr 93 MB/s nÀr man skriver 1 byte till en vanlig hÄrddisk ett mycket bra resultat, allt du behöver göra Àr att vÀlja den optimala buffertstorleken (i mitt fall Àr 256 KB precis lagom) och ersÀtta fwrite med _fwrite_nolock/fwrite_unlocked (ifall du inte behöver trÄdsÀkerhet förstÄs).
Det Ă€r samma sak med fread under liknande förhĂ„llanden. Eftersom jag inte har en maskin med Linux till hands (enkortsdatorer rĂ€knas inte), bestĂ€mde jag mig för att genomföra ett begrĂ€nsat experiment pĂ„ en virtuell maskin (Hyper-V, OpenSUSE 15, GCC 8.3.1) — mönstret Ă€r i princip detsamma: bara fwrite 20 Mb/s, fwrite + 256 KB buffert gav 23 Mb/s, fwrite_unlocked med samma buffert — 35 Mb/s (64-bitars binĂ€r, kompilerad med g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).

efterordet

Syftet med att skriva den hÀr artikeln var att beskriva en enkel och effektiv teknik i mÄnga fall (jag har aldrig stött pÄ funktionerna _fwrite_nolock/fwrite_unlocked förut, de Àr inte sÀrskilt populÀra - och förgÀves). Jag pÄstÄr inte att materialet Àr nytt, men jag hoppas att artikeln kommer att vara anvÀndbar för samhÀllet.

KĂ€lla: will.com

Köp pĂ„litlig hosting för webbplatser med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster