
Esipuhe
Maailmassa on niin yksinkertainen ja erittäin hyödyllinen apuohjelma - , 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 , 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 ...
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 - 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 - . 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):

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
