Rritja e përshpejtimit të futjes/sakrifikimit të skedarëve C\/C++, pa u lodhur shumë

Rritja e përshpejtimit të futjes/sakrifikimit të skedarëve C\/C++, pa u lodhur shumë

Parathënie

Ekziston njĂ« utilitar kaq i thjeshtĂ« dhe shumĂ« i dobishĂ«m nĂ« botĂ« — BDelta, dhe ndodhi qĂ« ai e ka gjetur rrugĂ«n e tij nĂ« procesin tonĂ« tĂ« prodhimit (sidoqoftĂ«, nuk arrita tĂ« instaloj versionin e saj, por ishin absolutisht jo versionet mĂ« tĂ« fundit). E pĂ«rdorim atĂ« pĂ«r qĂ«llimin e saj tĂ« drejtpĂ«rdrejtĂ« — ndĂ«rtimin e patches binarĂ«. NĂ«se e shikoni se çfarĂ« Ă«shtĂ« atje nĂ« depo, bĂ«het pak trishtues: nĂ« thelb, ajo Ă«shtĂ« braktisur qĂ« shumĂ« kohĂ« dhe shumĂ« gjĂ«ra aty janĂ« shumĂ« tĂ« vjetruara (pĂ«r njĂ« herĂ«, kolegu im i mĂ«parshĂ«m bĂ«ri disa ndryshime, por kjo ishte shumĂ« kohĂ« mĂ« parĂ«). Pra, vendosa ta ringjall kĂ«tĂ« projekt: bĂ«ra fork, hodha jashtĂ« atĂ« qĂ« nuk planifikoj tĂ« pĂ«rdor, dhe e kalova projektin nĂ« cmake, inkorporova mikrofunksionet "e nxehta", hoqa nga staku masat e mĂ«dha (dhe masat e ndryshueshme, nga tĂ« cilat mĂ« vinte sinqerisht keq), dhe e kalova pĂ«rsĂ«ri profiler-in — dhe zbulova se rreth 40% e kohĂ«s shpenzohej pĂ«r fwrite


ÇfarĂ« ndodhi me fwrite?

NĂ« kĂ«tĂ« kod fwrite (nĂ« rastin tim tĂ« veçantĂ«: ndĂ«rtimi i patch-it midis dy skedarĂ«ve tĂ« afĂ«rt me 300 MB, tĂ« dhĂ«nat hyrĂ«se krejtĂ«sisht nĂ« memorie) thirret miliona herĂ« me njĂ« buffer tĂ« vogĂ«l. ËshtĂ« e qartĂ« se ky gjĂ« do tĂ« ngadalĂ«sojĂ« punĂ«n, dhe prandaj do t'ia dalim qĂ« tĂ« ndikojmĂ« nĂ« kĂ«tĂ« situatĂ« pa dĂ«shiruar tĂ« implementojmĂ« lloje tĂ« ndryshme burimesh tĂ« dhĂ«nash, ose hyrje-dalje asinkron. E para qĂ« mĂ« erdhi nĂ« mendje — tĂ« rrisja madhĂ«sinĂ« e buffer-it

setvbuf(file, nullptr, _IOFBF, 64 * 1024)

por nuk kam marrĂ« ndonjĂ« pĂ«rmirĂ«sim tĂ« rĂ«ndĂ«sishĂ«m (tani fwrite merrte rreth 37% tĂ« kohĂ«s) — kĂ«shtu qĂ« problemi nuk Ă«shtĂ« nĂ« shkrimin e shpeshtĂ« tĂ« tĂ« dhĂ«nave nĂ« disk. Duke shqyrtuar "nĂ«n kapak" fwrite mund tĂ« shohim se brenda ndodhin njĂ« bllokim/zhbllokim i strukturĂ«s FILE nĂ« mĂ«nyrĂ« tĂ« tillĂ« (pseudokod, gjithĂ« analiza u krye 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);   /* blloko rrjedhën */
   __try
   {
      retval = _fwrite_nolock(buffer, size, count, stream);
   }
   __finally 
   {
       _unlock_str(stream);   /* zhblloko rrjedhën */
   }
   return retval;
}

NĂ«se i besoj profiler-it, nĂ« _fwrite_nolock ndodhin vetĂ«m 6% e kohĂ«s, pĂ«r pjesĂ«n tjetĂ«r — overhead. NĂ« rastin tim tĂ« veçantĂ«, siguria e rrjedhĂ«s Ă«shtĂ« njĂ« e tepricĂ« e dukshme, prandaj do ta sakrifikoj atĂ«, duke e zĂ«vendĂ«suar thirrjen fwrite me _fwrite_nolock — as even with arguments, there's no need to complicate things. In conclusion: this simple manipulation drastically reduced the costs of recording the result, which, in the original version, accounted for almost half of the time spent. By the way, there is a similar function in the POSIX world — fwrite_unlocked. Generally speaking, the same goes for fread. Thus, using a couple of #define, you can achieve a fairly cross-platform solution without unnecessary locks when they are not needed (and this happens quite often).

fwrite, _fwrite_nolock, setvbuf

Let's abstract from the original project and focus on testing a specific case: writing a large file (512 MB) in extremely small portions — 1 byte at a time. Test system: AMD Ryzen 7 1700, 16 GB RAM, HDD 7200 rpm with 64 MB cache, Windows 10 1809, 32-bit binary built, optimizations enabled, library statically linked.

Sample for conducting the experiment:


#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;
}

The variables will be TEST_BUFFER_SIZE, and for a couple of cases, we will replace fwrite_unlocked with fwrite. Let's start with the fwrite case without explicitly setting the buffer size (let's comment out setvbuf and related code): time 27048906 ”s, write speed — 18.93 MB/s. Now let's set the buffer size to 64 KB: time — 25037111 ”s, speed — 20.44 MB/s. Now we'll test _fwrite_nolock without calling setvbuf: 7262221 ”s, speed — 70.5 MB/s!

Next, let's experiment with buffer size (setvbuf):

Rritja e përshpejtimit të futjes/sakrifikimit të skedarëve C\/C++, pa u lodhur shumë

The data was obtained by averaging 5 experiments; I was too lazy to account for inaccuracies. As for me, 93 MB/s when writing 1 byte on a regular HDD is quite a good result, you just need to choose the optimal buffer size (for me, 256 KB is just right) and replace fwrite with _fwrite_nolock/fwrite_unlocked (if thread safety is not needed, of course).
Similarly with fread under such conditions. Since I don't have a 'metal' machine with Linux at hand (single-board computers don't count), I decided to conduct a limited experiment on a virtual machine (Hyper-V, OpenSUSE 15, GCC 8.3.1) — the pattern, in principle, is the same: 'bare' fwrite gives 20 MB/s, fwrite + 256 KB buffer produced 23 MB/s, fwrite_unlocked with the same buffer — 35 MB/s (64-bit binary built with g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).

Pasthënie

QĂ«llimi i kĂ«tij artikulli ishte pĂ«rshkrimi i njĂ« metode tĂ« thjeshtĂ« dhe efektive nĂ« shumĂ« raste (me funksionet _fwrite_nolock/fwrite_unlocked nuk kam pasur ndonjĂ«herĂ« tĂ« bĂ«j, nuk janĂ« shumĂ« tĂ« njohura – por kot). Nuk pretendoj pĂ«r novacion tĂ« materialit, por shpresoj qĂ« artikulli do tĂ« jetĂ« i dobishĂ«m pĂ«r komunitetin.

Burimi: habr.com

Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster