Failide sisendi- ja vÀljalaske kiirus C/C++-ga, ilma liialt pingutamata

Failide sisendi- ja vÀljalaske kiirus C/C++-ga, ilma liialt pingutamata

EessÔna

On maailmas ĂŒks lihtne ja vĂ€ga kasulik utiliit — BDelta, ja nii juhtuski, et see on juba ammu juurdunud meie tootmisprotsessi (tĂ”si, selle versiooni installimine ei Ă”nnestunud, kuid see ei olnud kindlasti viimase saadaval versioon). Kasutame seda oma algses otstarbes - binaarsete patchide loomisel. Kui vaadata, mis seal repos on, siis muutub pisut kurvaks: pĂ”himĂ”tteliselt on see juba ammu maha jĂ€etud ja palju seal on tugevalt vananenud (kunagi tegi sinna mitmeid parandusi mu endine kolleeg, kuid see juhtus kaua aega tagasi). ÜhesĂ”naga, otsustasin selle asja elustada: forkisin, viskasin vĂ€lja selle, mida ei kavatse kasutada, ning tĂ”in projekti ĂŒle cmake, inlinesin "kuumad" mikrofunktsioonid, eemaldasin virnast suured massiivid (ja muutuva pikkusega massiivid, millest mul tĂ”eliselt "pöördub"), lĂ€bisin uuesti profiilerit - ja sain teada, et umbes 40% ajast kulub fwrite


Mis seal fwrite’iga siis toimub?

Antud koodis kutsutakse fwrite (minu konkreetses testjuhtumis: patchi loomine sarnaste 300 MB failide vahel, sisendandmed on tĂ€ielikult mĂ€lus) miljoneid kordi vĂ€lja vĂ€ikese suurusega puhvriga. On ilmselge, et see asi hakkab kĂ”vasti segama, ja seetĂ”ttu tahaks kuidagi selle asja ĂŒle mĂ”jutada. Erinevate andmeallikate, asĂŒnkroonse I/O rakendamine ei ole hetkel soovitav, tahaks leida lihtsamat lahendust. Esimene asi, mis pĂ€he tuli — suurendada puhversuurust

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

aga tulemus ei paranenud oluliselt (nĂŒĂŒd kulus fwrite’ile umbes 37% ajast) - tĂ€hendas, et asi ei ole siiski sagedases andmete salvestamises kettale. Vaatamata fwrite “kapoti alla” vĂ”ib nĂ€ha, et seal toimub FILE struktuuri lukustamine/avalukustamine umbes nii (pseudokood, 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);   	/* lock stream */
   __try
   {
      retval = _fwrite_nolock(buffer, size, count, stream);
   }
   __finally 
   {
       _unlock_str(stream);   	/* unlock stream */
   }
   return retval;
}

Kui uskuda profiilerit, siis _fwrite_nolock’ile kulub vaid 6% ajast, ĂŒlejÀÀnud on overhead. Minu konkreetses jutuhoos on samal ajal vooluohutus ilmselge ĂŒleliigsus, selle ohverdan, asendades fwrite’i nimetuse _fwrite_nolock — isegi argumentide kasutamine ei ole vajalik. KokkuvĂ”tteks: see lihtne manipuleerimine vĂ€hendas oluliselt tulemuse salvestamise kulusid, mis originaalses variandis moodustasid peaaegu poole ajakuludest. Muide, POSIX maailmas on olemas sarnane funktsioon — fwrite_unlocked. Üldiselt kehtib sama ka freadi kohta. Seega on paar #define'i abil vĂ”imalik saada tĂ€iesti platvormide vahel töötav lahendus ilma liigsete lukustusteta, kui need ei ole vajalikud (mis toimub ĂŒsna sageli).

fwrite, _fwrite_nolock, setvbuf

LĂ€hme originaalsest projektist eemale ja keskendume konkreetse juhtumi testimisele: suure faili (512 MB) salvestamine ÀÀrmiselt vĂ€ikeste portsjonitena — 1 bait. TestisĂŒsteem: AMD Ryzen 7 1700, 16 GB RAM, HDD 7200 rpm 64 MB vahemĂ€lu, Windows 10 1809, binaar ehitatud 32-bitine, optimeerimised on sisse lĂŒlitatud, teek on staatiliselt lingitud.

Katseproov eksperimentide lÀbiviimiseks:


#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 ning paaril juhul asendame fwrite_unlocked fwrite'iga. Alustame fwrite juhtumist, ilma et oleksime selgelt bufferi suurust seadnud (kommenteerime vĂ€lja setvbuf ja sellega seotud koodi): aeg 27048906 ”s, kirjutamise kiirus — 18.93 MB/s. NĂŒĂŒd seadistame bufferi suuruseks 64 KB: aeg — 25037111 ”s, kiirus — 20.44 MB/s. Testime nĂŒĂŒd _fwrite_nolock'i setvbuf'i kutsumata: 7262221 ”s, kiirus — 70.5 MB/s!

Edasi katsetame bufferi suurusega (setvbuf):

Failide sisendi- ja vÀljalaske kiirus C/C++-ga, ilma liialt pingutamata

Andmed on saadud 5 katse keskmistamisel, vigu ma ei arvestanud. Minu arvates on 93 MB/s kirjutamine 1 baitiga tavalisele HDD-le vĂ€ga hea tulemus, tuleb vaid valida optimaalne bufferi suurus (minu jaoks 256 KB — ideaalne) ja asendada fwrite _fwrite_nolock'i vĂ”i fwrite_unlocked'iga (kui voogude ohutus muidugi ei ole vajalik).
Sarnane kehtib fread'i kohta sarnastes tingimustes. Kuna mul ei ole kĂ€sitsi linuxi 'raudmasinat' (ĂŒheplaadilised ei kvalifitseeru), otsustasin lĂ€bi viia piiratud eksperimendi virtuaalmasinas (Hyper-V, OpenSUSE 15, GCC 8.3.1) — seaduspĂ€rasus on pĂ”himĂ”tteliselt sama: 'paljas' fwrite 20 MB/s, fwrite + 256 KB buffer andis 23 MB/s, fwrite_unlocked sama bufferiga — 35 MB/s (binaar 64-bitine, kompileeritud g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).

EessÔna

Selle artikli eesmĂ€rk oli kirjeldada lihtsat ja tĂ”husat meetodit, mis töötab paljudes olukordades (funktsioonidega _fwrite_nolock/fwrite_unlocked, millega ma enne ei olnud kokku puutunud, ei ole nad just vĂ€ga populaarsed — kuid asjata). Uue materjali osas ma pretentsiooni ei oma, kuid loodan, et artikkel osutub kogukonnale kasulikuks.

Allikas: habr.com

Osta usaldusvÀÀrne veebimajutus DDoS-kaitsega veebisaitidele, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebimajutus DDoS-kaitsega veebisaitidele, VPS VDS serverid - ProHoster