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