Nopeuta C/C++-tiedostojen I/O-toimintoa hikoilematta

Nopeuta C/C++-tiedostojen I/O-toimintoa hikoilematta

Esipuhe

Maailmassa on niin yksinkertainen ja erittäin hyödyllinen apuohjelma - BDelta, ja niin tapahtui, että se oli juurtunut tuotantoprosessiimme hyvin pitkään (vaikka sen versiota ei ollut mahdollista asentaa, mutta se ei todellakaan ollut viimeinen saatavilla oleva). Käytämme sitä aiottuun tarkoitukseen - binaarikorjausten rakentamiseen. Jos katsot arkiston sisältöä, siitä tulee hieman surullista: itse asiassa se hylättiin kauan sitten ja suuri osa siitä on hyvin vanhentunutta (entinen kollegani teki siellä useita muokkauksia, mutta siitä oli kauan aikaa) . Yleensä päätin herättää tämän asian henkiin: haaroitin, heitin pois sen, mitä en aikonut käyttää, siirsin projektin cmake, sisälsi "kuumat" mikrotoiminnot, poisti pinosta suuret taulukot (ja vaihtelevan pituiset taulukot, jotka suoraan sanottuna tekevät minusta "pommin"), suoritti profiloijan uudelleen - ja huomasi, että noin 40 % ajasta kuluu fwrite...

Mitä fwritelle sitten kuuluu?

Tässä koodissa fwritea (omassa testitapauksessani: kun rakennetaan korjaustiedosto lähes 300 MB tiedostojen väliin, syötetiedot ovat kokonaan muistissa) kutsutaan miljoonia kertoja pienellä puskurilla. Ilmeisesti tämä asia hidastuu, ja siksi haluaisin jotenkin vaikuttaa tähän häpeään. Erilaisia ​​tietolähteitä ei ole vielä haluttu toteuttaa, asynkroninen input-output, halusin löytää yksinkertaisemman ratkaisun. Ensimmäisenä tuli mieleen puskurin koon kasvattaminen

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

mutta en saanut merkittävää parannusta tulokseen (nyt fwrite oli noin 37% ajasta) - mikä tarkoittaa, että kyse ei silti ole siitä, että tietoja kirjoitetaan usein levylle. Kun katsot fwrite-ohjelman "hupun alta", voit nähdä, että lukitus/avaa TIEDOSTO-rakennetta tapahtuu jotain tämän kaltaista (pseudokoodi, kaikki analyysi suoritettiin Visual Studio 2017:ssä):


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

Profiloijan mukaan _fwrite_nolockin osuus ajasta on vain 6 %, loput ovat yleiskustannuksia. Omassa tapauksessani lankojen turvallisuus on selvästi ylivoimaista, joten uhraan sen korvaamalla fwrite-puhelun _fwrite_nolock - Sinun ei tarvitse edes olla älykäs argumenttien kanssa. Yhteensä: tämä yksinkertainen manipulointi alensi merkittävästi tuloksen tallennuskustannuksia, jotka alkuperäisessä versiossa olivat lähes puolet käytetystä ajasta. Muuten, POSIX-maailmassa on samanlainen toiminto - fwrite_unlocked. Yleisesti ottaen sama pätee freadiin. Siten käyttämällä #defines-paria voit saada täysin monialustaisen ratkaisun ilman tarpeettomia lukkoja, jos ne eivät ole välttämättömiä (ja tätä tapahtuu melko usein).

fwrite, _fwrite_nolock, setvbuf

Siirrytään pois alkuperäisestä projektista ja keskitytään testaamaan tiettyä tapausta: suuren tiedoston (512 Mt) kirjoittamista erittäin pienissä osissa – yksi tavu kerrallaan. Testijärjestelmä: AMD Ryzen 7 1700, 16 Gt RAM-muistia, 7200 rpm kiintolevy, 64 Mt välimuisti. Windows 10 Vuonna 1809 binääritiedosto rakennettiin 32-bittiseksi, optimoinnit ovat käytössä ja kirjasto on staattisesti linkitetty.

Esimerkki kokeeseen:


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

Muuttujat ovat TEST_BUFFER_SIZE, ja parissa tapauksessa korvaamme fwrite_unlocked fwritella. Aloitetaan fwrite-tapauksesta asettamatta erikseen puskurin kokoa (kommentoi setvbuf ja siihen liittyvä koodi): aika 27048906 µs, kirjoitusnopeus - 18.93 MB/s. Asetetaan nyt puskurin kooksi 64 KB: aika - 25037111 μs, nopeus - 20.44 Mb/s. Testataan nyt _fwrite_nolockin toimintaa kutsumatta setvbufia: 7262221 µs, nopeus - 70.5 Mb/s!

Seuraavaksi kokeillaan puskurin kokoa (setvbuf):

Nopeuta C/C++-tiedostojen I/O-toimintoa hikoilematta

Tiedot saatiin laskemalla 5 kokeen keskiarvo. Olin liian laiska laskemaan virheitä. Minulle 93 MB/s kirjoitettaessa 1 tavu tavalliselle kiintolevylle on erittäin hyvä tulos, sinun tarvitsee vain valita optimaalinen puskurin koko (minun tapauksessani 256 KB on juuri sopiva) ja korvata fwrite arvolla _fwrite_nolock/fwrite_unlocked ( jos lankaturvaa ei tietenkään tarvita).
Samoin freadin kanssa vastaavissa olosuhteissa. Koska minulla ei ole Linuxilla varustettua laitteistoa käsillä (yksilevyisiä tietokoneita ei lasketa), päätin tehdä rajoitetun kokeen virtuaalikoneella (Hyper-V, OpenSUSE 15, GCC 8.3.1) - kuvio on periaatteessa sama: "alaston" fwrite 20 Mb/s, fwrite + 256 KB puskuri tuotettu 23 Mb/s, fwrite_unlocked samalla puskurilla - 35 Mb/s (64-bittinen, koottu g++ -o2 - s -static-libgcc -static-libstdc++ fwrite_test.

loppusanat

Tämän artikkelin kirjoittamisen tarkoituksena oli kuvata yksinkertaista ja tehokasta tekniikkaa monissa tapauksissa (en ole koskaan törmännyt _fwrite_nolock/fwrite_unlocked-funktioihin, ne eivät ole kovin suosittuja - mutta turhaan). En väitä, että materiaali on uutta, mutta toivon, että artikkelista on hyötyä yhteisölle.

Lähde: will.com

Osta luotettava isännöinti sivustoille, joissa on DDoS-suojaus, VPS VDS -palvelimet 🔥 Osta luotettavaa verkkosivustojen hostingia DDoS-suojauksella, VPS VDS -palvelimilla | ProHoster