
EessÔna
On maailmas ĂŒks lihtne ja vĂ€ga kasulik utiliit â , 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 , 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 âŠ
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 â 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 â . Ă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):

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
