
Parathënie
Ka njihet një utilitar kaq i thjeshtë dhe shumë të dobishëm — , dhe ndodhi që ai është thellësisht i instaluar në procesin tonë prodhues (e vërteta është se nuk arritëm ta instalojmë versionin e tij, por me siguri nuk ishte ai më i fundit). E përdorim për qëllimin e tij të drejtpërdrejtë — ndërtimin e patch-ave binarë. Nëse shikojmë çfarë ka atje në depo, bëhet pak e trishtueshme: në thelb, ajo është braktisur prej kohësh dhe shumë prej saj janë tëDataset e vjetra (disa modifikime janë bërë aty nga ish-kolegu im, por ka qenë kohë e gjatë). Pra, vendosa ta ringjall këtë: e forkuam, hoqa atë që nuk planifikoj të përdor, e tërhoqa projektin në , kam inlinuar mikrofunksionet “e nxehta”, heqa nga skedari array-t e mëdha (dhe array-t me gjatësi variabël, që më shqetësojnë pa fund), e kam kaluar përsëri profilerin — dhe kuptova se rreth 40% e kohës shpenzohet për …
Çfarë ndodh me fwrite?
Në këtë kod fwrite (në rastin tim konkret: ndërtimi i një patch-i midis dy skedarëve të ngjashëm prej 300 Mb, të dhënat totale në memorie) thirret miliona herë me një buffer të vogël. Është e qartë se kjo do të ngadalësohet, dhe prandaj do të doja të ndikoj në këtë pa e komplikuar situatën. Nuk kam dëshirë të integroj lloje të ndryshme burimesh të dhënash, I/O asinkron, desha të gjej një zgjidhje më të thjeshtë. E para që më erdhi në mendje ishte të rrisja madhësinë e buffer-it
setvbuf(file, nullptr, _IOFBF, 64 * 1024)por nuk arrita ndonjë përmirësim të dukshëm të rezultatit (tani fwrite kishte rreth 37% të kohës) — kjo tregon se problemi nuk është shpesh në shkrimin e të dhënave në disk. Duke shqyrtuar “nën kapak” fwrite, mund të shihni se brenda ndodhin lock/unlock të strukturës FILE përafërsisht kështu (pseudokod, gjithë analiza është bërë nën 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;
}
Nëse i besoj profilerit, _fwrite_nolock merr vetëm 6% të kohës, e gjithë pjesa tjetër është overhead. Në rastin tim konkret, siguria e thread-it është një luks i tepruar, të cilin do ta sakrifikoj duke zëvendësuar thirrjen fwrite me — as nuk ka nevojë të luaj me argumentet. Pra, kjo manipulim e thjeshtë ndihmoi shumë në zvogëlimin e shpenzimeve për të shkruar rezultatin, që në variantin fillestar përbënte pothuajse gjysmën e kohës totale. Për të qenë i saktë, në botën POSIX ka një funksion të ngjashëm — . Në përgjithësi, kjo vlen edhe për fread. Kështu, duke përdorur disa #define mund të arrijmë një zgjidhje krejtësisht të platformave pa bllokime të panevojshme në rastet kur ato nuk janë të nevojshme (ndodhin shpesh).
fwrite, _fwrite_nolock, setvbuf
Le të abstrahohemi nga projekti origjinal dhe të merremi me testimin e një rasti të veçantë: shkrimi i një skedari të madh (512 Mb) në porsona shumë të vegjël — në 1 byte. Sistemi testues: AMD Ryzen 7 1700, 16 GB RAM, HDD 7200 rpm 64 Mb cache, Windows 10 1809, binari ishte ndërtuar 32-bit, optimizimet ishin aktivizuar, biblioteka ishte e lidhur statikisht.
Shembulli për të kryer eksperimentin:
#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;
}
Si variabla do të shërbejnë TEST_BUFFER_SIZE, dhe gjithashtu për disa raste do të zëvendësojmë fwrite_unlocked me fwrite. Le të fillojmë me rastin fwrite pa vendosur madhësinë e buffer-it (do ta komentojmë setvbuf dhe kodin përkatës): koha 27048906 μs, shpejtësia e shkrimit — 18.93 Mb/s. Tani do të vendosim madhësinë e buffer-it në 64 KB: koha — 25037111 μs, shpejtësia — 20.44 Mb/s. Tani do të testojmë funksionimin e _fwrite_nolock pa thirrjen e setvbuf: 7262221 μs, shpejtësia — 70.5 Mb/s!
Pastaj do të eksperimentojmë me madhësinë e buffer-it (setvbuf):

Të dhënat janë marrë nga mesatarizimi i 5 eksperimenteve, nuk u mundova të llogaris ndonjë gabim. Sipas meje, 93 Mb/s kur shkruhet 1 byte në një HDD të zakonshëm — kjo është një rezultat shumë i mirë, vetëm duhet të zgjidhni madhësinë optimale të buffer-it (në rastin tim 256 KB — është më se e përshtatshme) dhe zëvendësoni fwrite me _fwrite_nolock/fwrite_unlocked (në rast se nuk kërkohet siguria e thread-it, sigurisht).
po ashtu edhe për fread në këto kushte. Duke parë se nuk kam një makinë fizike me linux pranë (nje laptop eshte marre parasysh), vendosa të kryej një eksperiment të kufizuar në një makinë virtuale (Hyper-V, OpenSUSE 15, GCC 8.3.1) — lidhja është gjithashtu e njëjtë: fwrite e pastër 20 Mb/s, fwrite + buffer në 256 KB dha 23 Mb/s, fwrite_unlocked me të njëjtin buffer — 35 Mb/s (binari 64-bit, u ndërtua g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).
Pasthënie
Qëllimi i kësaj artikulli ishte përshkrimi i një truku të thjeshtë dhe efektiv në shumë raste (me funksionet _fwrite_nolock/fwrite_unlocked nuk isha përballur më parë, nuk janë shumë popullore — dhe kot). Nuk pretendoj për novatorizëm, por shpresoj që artikulli do të jetë i dobishëm për komunitetin.
Burimi: habr.com
