
Predgovor
Na svetu obstaja tako preprost in zelo uporaben pripomoček - , in zgodilo se je, da je bil že zelo dolgo zasidran v našem proizvodnem procesu (čeprav njegove različice ni bilo mogoče namestiti, vsekakor pa ni bila zadnja na voljo). Uporabljamo ga za predvideni namen - gradnjo binarnih popravkov. Če pogledate, kaj je v repozitoriju, postane malo žalostno: pravzaprav je bilo že zdavnaj opuščeno in veliko je zelo zastarelo (moj nekdanji kolega je tam nekoč večkrat urejal, vendar je bilo to že dolgo nazaj) . Na splošno sem se odločil oživiti to zadevo: razcepil sem se, vrgel ven tisto, česar nisem nameraval uporabiti, projekt premaknil na , vnesel "vroče" mikrofunkcije, odstranil velike nize iz sklada (in nize spremenljive dolžine, zaradi katerih sem odkrito "bomba"), še enkrat zagnal profiler - in ugotovil, da se približno 40 % časa porabi za ...
Kaj je torej s fwrite?
V tej kodi je fwrite (v mojem posebnem testnem primeru: izdelava popravka med tesnimi 300 MB datotekami, vhodni podatki so v celoti v pomnilniku) poklicana milijonkrat z majhno velikostjo medpomnilnika. Očitno se bo ta stvar upočasnila in zato bi rad nekako vplival na to sramoto. Ni še želje po implementaciji različnih vrst podatkovnih virov, asinhronih I/O, želel sem najti enostavnejšo rešitev. Prva stvar, ki mi je prišla na misel, je bila povečati velikost medpomnilnika
setvbuf(file, nullptr, _IOFBF, 64* 1024)vendar nisem dosegel bistvenega izboljšanja rezultata (zdaj je fwrite predstavljal približno 37 % časa) - kar pomeni, da še vedno ne gre za pogosto pisanje podatkov na disk. Če pogledate "pod pokrov" fwrite, lahko vidite, da se struktura FILE za zaklepanje/odklepanje dogaja znotraj nečesa takega (psevdo koda, vsa analiza je bila izvedena 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;
}
Po podatkih profilerja _fwrite_nolock predstavlja le 6 % časa, ostalo so režijski stroški. V mojem posebnem primeru je varnost niti očitno pretirana, zato jo bom žrtvoval tako, da bom klic fwrite zamenjal z - sploh ni treba biti pameten z argumenti. Skupaj: ta preprosta manipulacija je znatno zmanjšala stroške beleženja rezultata, ki je v prvotni različici znašal skoraj polovico porabljenega časa. Mimogrede, v svetu POSIX obstaja podobna funkcija - . Na splošno velja enako za fread. Tako lahko z uporabo para #defines dobite popolnoma večplatformsko rešitev brez nepotrebnih zaklepanj, če niso potrebne (in to se zgodi precej pogosto).
fwrite, _fwrite_nolock, setvbuf
Odmaknimo se od prvotnega projekta in se osredotočimo na testiranje specifičnega primera: zapisovanje velike datoteke (512 MB) v izjemno majhnih delih – en bajt na del. Testni sistem: AMD Ryzen 7 1700, 16 GB RAM-a, trdi disk s 7200 vrtljaji na minuto, 64 MB predpomnilnika. Windows 10 1809, binarna datoteka je bila zgrajena kot 32-bitna, optimizacije so omogočene, knjižnica je statično povezana.
Vzorec za poskus:
#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;
}
Spremenljivke bodo TEST_BUFFER_SIZE in v nekaj primerih bomo fwrite_unlocked zamenjali s fwrite. Začnimo s primerom fwrite brez izrecne nastavitve velikosti vmesnega pomnilnika (zakomentirajte setvbuf in povezano kodo): čas 27048906 µs, hitrost zapisovanja - 18.93 MB/s. Zdaj nastavimo velikost medpomnilnika na 64 KB: čas - 25037111 μs, hitrost - 20.44 Mb/s. Zdaj pa preizkusimo delovanje _fwrite_nolock brez klica setvbuf: 7262221 µs, hitrost - 70.5 Mb/s!
Nato poskusimo z velikostjo medpomnilnika (setvbuf):

Podatki so bili pridobljeni s povprečenjem 5 poskusov; bil sem prelen, da bi izračunal napake. Kar se mene tiče, je 93 MB/s pri zapisovanju 1 bajta na običajni HDD zelo dober rezultat, le izbrati morate optimalno velikost medpomnilnika (v mojem primeru je 256 KB ravno prav) in zamenjati fwrite z _fwrite_nolock/fwrite_unlocked ( seveda v primeru, da varnost niti ni potrebna).
Enako s freadom v podobnih razmerah. Ker pri roki nimam strojne opreme z Linuxom (posamezne plošče ne štejejo), sem se odločil izvesti omejen eksperiment na virtualnem stroju (Hyper-V, OpenSUSE 15, GCC 8.3.1) - vzorec je v bistvu enako: “goli” fwrite 20 Mb/s, fwrite + 256 KB medpomnilnik proizvede 23 Mb/s, fwrite_unlocked z istim medpomnilnikom - 35 Mb/s (64-bitni binarni, sestavljeni g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test. cpp -o fwrite_test).
spremna beseda
Namen pisanja tega članka je bil opisati preprosto in v mnogih primerih učinkovito tehniko (še nikoli nisem naletel na funkcije _fwrite_nolock/fwrite_unlocked, niso zelo priljubljene - a zaman). Ne trdim, da je gradivo novo, vendar upam, da bo članek koristen skupnosti.
Vir: www.habr.com
