C/C++ failide sisestamise ja väljundi kiirus, ilma liialdamata

C/C++ failide sisestamise ja väljundi kiirus, ilma liialdamata

Eessõna

Maailmas on olemas selline lihtne ja väga kasulik utiliit — BDelta, ja nii on juhtunud, et see on ammusest ajast meie tootmisprotsessis juurdunud (kuigi selle versiooni installimine ei õnnestunud, polnud see kindlasti viimane saadaval). Kasutame seda otstarbekohaselt — binaarsete patch'ide loomine. Kui vaadata, mis seal hoidlas on, — tekib veidi kurbus: põhimõtteliselt on see ammu hüljatud ja palju on seal tugevalt vananenud (kunagi tegi sinna paar muudatust minu endine kolleeg, aga see oli ammu). Ühesõnaga, otsustasin seda ellu äratada: tegin fork'i, viskasin minema selle, mida ei plaani kasutada, ja viisin projekti üle cmake, inline'isin „kuumad“ mikrofunktsioonid, kustutasin virnast suured massiivid (ja muutlikud massiivid, mis mind tõeliselt „raevu ajavad”), jooksutasin uuesti profilaatorit — ja sain teada, et umbes 40% ajast kulub fwrite

Nii et kuidas on fwrite'iga?

Selles koodis kutsutakse fwrite (minu konkreetse testi juhul: 300 MB suuruste failide patchi loomine, sisend on täielikult mälus) miljonite kordadega väikese suurusega puhvris. On selge, et see asi tõmbab pidurit, seega sooviks leida lahenduse, mis aitaks olukorda parandada, kuid soovimata rakendada erinevaid andmeallikaid ega asünkroonset sisend-väljundit. Esimene mõte, mis pähe tuli, oli puhvri suuruse suurendamine.

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

kuid oluliselt paremat tulemust ei saanud (nüüd kulus fwrite'ile umbes 37% ajast) — see tähendab, et probleem ei ole andmete sagedases kirjutamises kettale. Vaadates fwrite'i „kapoti alla” võib näha, et seal toimub FILE struktuuri lukustamine ja lukust vabastamine umbes nii (pseudo-kood, kogu analüüs viidi läbi Visual Studio 2017 all):


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

Kui uskuda profiilimist, siis _fwrite_nolock hõivab vaid 6% ajast, ülejäänud on overhead. Minu konkreetses juhtumis on lõimede turvalisus ilmselt üleliigne, millest ma loobun, asendades fwrite väljakutsega _fwrite_nolock — isegi argumentide kallal ei pea nuputama. Kokkuvõttes: see lihtne manipulatsioon vähendas tulemuse kirjutamise kulusid mitu korda, mis algses variandis moodustasid peaaegu poole ajakulu. Muide, POSIX-i maailmas on sarnane funktsioon — fwrite_unlocked. Üldiselt puudutab see sama ka freadi. Seega, paar #define abil on võimalik saavutada täiesti platvormidevaheline lahendus ilma liigsete lukustusteta, kui neid ei ole vaja (mis juhtub väga sageli).

fwrite, _fwrite_nolock, setvbuf

Haarame originaalprojektist kaugemale ja tegeleme konkreetse juhtumi testimisega: suurte failide (512 MB) salvestamine äärmiselt väikestes portsjonites — 1 bait. Testisüsteem: AMD Ryzen 7 1700, 16 GB RAM, HDD 7200 rpm 64 MB vahemälu, Windows 10 1809, binaar koostati 32-bitises versioonis, optimeerimine on sisse lülitatud, raamatukogu on staatiliselt lingitud.

Eksperimendi läbiviimise näidis:


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

Muutujateks on TEST_BUFFER_SIZE, samuti asendame fwrite_unlocked juhul fwrite'iga. Alustame kirjutamisest ilma leidliku puhver suuruse määramiseta (kommenteerime setvbuf ja sellega seotud koodi välja): aeg 27048906 μs, kirjutamise kiirus — 18.93 MB/s. Nüüd seame puhvri suuruseks 64 KB: aeg — 25037111 μs, kiirus — 20.44 MB/s. Järgmiseks katsetame _fwrite_nolock'i kasutamist ilma setvbuf'i kutsumata: 7262221 μs, kiirus — 70.5 MB/s!

Vaatame edasi puhvri suuruse (setvbuf) katsetamist:

C/C++ failide sisestamise ja väljundi kiirus, ilma liialdamata

Andmed on saadud 5 katse keskmise põhjal, viga ei hakanud küll arvutama. Minu arvates on 93 MB/s andmete kirjutamisel 1 baidi kaupa tavalisele HDD-le väga hea tulemus; tuleb vaid valida optimaalse puhvri suurus (minu puhul 256 KB - just õigesti) ja asendada fwrite _fwrite_nolock'iga/fwrite_unlocked'iga (kui järjepidevuse ohutus pole vajalik, loomulikult).
Sarnane fread'ile sarnastes tingimustes. Kuna „raud“ masinat Linuxiga käepärast ei ole (üheplaadilised arvutid ei loe), otsustasin teha piiratud katse virtuaalmasinas (Hyper-V, OpenSUSE 15, GCC 8.3.1) — seaduspärasus on põhimõtteliselt sama: „palja“ fwrite kiirus on 20 MB/s, fwrite + 256 KB puhver andis 23 MB/s, fwrite_unlocked sama puhvri jaoks — 35 MB/s (binaar 64-bitine, koostasin g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).

Järelsõna

Selle artikli eesmärk oli kirjeldada lihtsat ja tõhusat lähenemist, mis töötab paljudes olukordades (funktsioonidega _fwrite_nolock/fwrite_unlocked ei olnud mul varem kokku puutunud, need ei ole väga populaarsed — ja kahju). Uudsuse osas ei pretendeeri, kuid loodan, et artikkel osutub kogukonnale kasulikuks.

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster