
Predgovor
Na svijetu postoji tako jednostavan i vrlo koristan uslužni program - , a dogodilo se da je bio ukorijenjen u našem proizvodnom procesu jako dugo (iako nije bilo moguće instalirati njegovu verziju, ali definitivno nije bila posljednja dostupna). Koristimo ga za njegovu namjenu - izgradnju binarnih zakrpa. Ako pogledate šta se nalazi u spremištu, postaje malo tužno: u stvari, odavno je napušteno i veliki dio je jako zastario (moj bivši kolega je jednom tamo napravio nekoliko izmjena, ali to je bilo davno) . Generalno, odlučio sam da oživim ovu stvar: račvao sam se, izbacio ono što nisam planirao da koristim, preselio projekat na , umetnuo "vruće" mikrofunkcije, uklonio velike nizove iz steka (i nizove promjenjive dužine, što me iskreno čini "bombom"), još jednom pokrenuo profiler - i otkrio da se oko 40% vremena troši na ...
Pa šta je sa fwrite-om?
U ovom kodu, fwrite (u mom specifičnom testnom slučaju: pravljenje zakrpe između blizu 300 MB fajlova, ulazni podaci su u potpunosti u memoriji) se poziva milionima puta sa malom veličinom bafera. Očigledno da će se ovo usporiti, i zato bih želeo da nekako utičem na ovu sramotu. Još nema želje za implementacijom raznih vrsta izvora podataka, asinhronog ulaza-izlaza, htio sam pronaći jednostavnije rješenje. Prvo što mi je palo na pamet je povećanje veličine bafera
setvbuf(file, nullptr, _IOFBF, 64* 1024)ali nisam dobio značajno poboljšanje u rezultatu (sada je fwrite činio oko 37% vremena) - što znači da još uvijek nije stvar čestog pisanja podataka na disk. Gledajući “ispod haube” fwrite-a, možete vidjeti da se struktura FILE zaključavanja/otključavanja događa unutar nečega poput ovoga (pseudo-kod, sva analiza je obavljena pod Visual Studio 2017):
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;
}
Prema profileru, _fwrite_nolock čini samo 6% vremena, ostalo je prekomjerno. U mom konkretnom slučaju, sigurnost niti je očigledno prevelika, pa ću je žrtvovati zamjenom poziva fwrite sa - ne morate čak ni da budete pametni sa argumentima. Ukupno: ova jednostavna manipulacija značajno je smanjila troškove snimanja rezultata, koji su u originalnoj verziji iznosili skoro polovinu utrošenog vremena. Inače, u POSIX svijetu postoji slična funkcija - . Uopšteno govoreći, isto se odnosi i na fread. Dakle, koristeći par #defines, možete dobiti potpuno višeplatformsko rješenje bez nepotrebnih zaključavanja ako nisu neophodne (a to se događa prilično često).
fwrite, _fwrite_nolock, setvbuf
Udaljimo se od originalnog projekta i fokusirajmo se na testiranje specifičnog slučaja: pisanje velike datoteke (512 MB) u izuzetno malim dijelovima - po jedan bajt. Testni sistem: AMD Ryzen 7 1700, 16 GB RAM-a, 7200 rpm HDD, 64 MB keš memorije. Windows 10 1809, binarna datoteka je izgrađena kao 32-bitna, optimizacije su omogućene, biblioteka je statički povezana.
Uzorak za eksperiment:
#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;
}
Varijable će biti TEST_BUFFER_SIZE, a u nekoliko slučajeva ćemo zamijeniti fwrite_unlocked sa fwrite. Počnimo sa slučajem fwrite bez eksplicitnog postavljanja veličine bafera (komentirajte setvbuf i pridruženi kod): vrijeme 27048906 µs, brzina pisanja - 18.93 MB/s. Sada postavimo veličinu bafera na 64 KB: vrijeme - 25037111 μs, brzina - 20.44 Mb/s. Sada testirajmo rad _fwrite_nolock bez pozivanja setvbuf-a: 7262221 µs, brzina - 70.5 Mb/s!
Zatim, eksperimentirajmo s veličinom bafera (setvbuf):

Podaci su dobijeni u prosjeku 5 eksperimenata; bio sam previše lijen da izračunam greške. Što se mene tiče, 93 MB/s pri pisanju 1 bajta na običan HDD je jako dobar rezultat, samo trebate odabrati optimalnu veličinu bafera (u mom slučaju 256 KB je taman) i zamijeniti fwrite sa _fwrite_nolock/fwrite_unlocked ( u slučaju da sigurnost niti nije potrebna, naravno).
Isto tako i sa fredom u sličnim uslovima. Pošto nemam pri ruci hardversku mašinu sa Linuxom (jednopločni računari se ne računaju), odlučio sam da sprovedem ograničen eksperiment na virtuelnoj mašini (Hyper-V, OpenSUSE 15, GCC 8.3.1) - šablon je, u principu, isti: “goli” fwrite 20 Mb/s, fwrite + 256 KB bafer proizvodi 23 Mb/s, fwrite_unlocked sa istim baferom - 35 Mb/s (64-bitno binarno, sklopljeno g++ -o2 - s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).
Posle reči
Svrha pisanja ovog članka bila je opisati jednostavnu i efikasnu tehniku u mnogim slučajevima (nikad ranije nisam naišao na funkcije _fwrite_nolock/fwrite_unlocked, nisu baš popularne - ali uzalud). Ne pretvaram se da je materijal nov, ali se nadam da će članak biti koristan zajednici.
izvor: www.habr.com
