Paātriniet C/C++ failu ievadi/izvadi, nezaudējot sviedrus

Paātriniet C/C++ failu ievadi/izvadi, nezaudējot sviedrus

priekŔvārds

Pasaulē ir tik vienkārÅ”a un ļoti noderÄ«ga utilÄ«ta - BDelta, un sagadÄ«jās, ka mÅ«su ražoÅ”anas procesā tas bija iesakņojies ļoti ilgu laiku (lai gan tā versiju nebija iespējams uzstādÄ«t, bet tā noteikti nebija pēdējā pieejamā). Mēs to izmantojam paredzētajam mērÄ·im - bināro ielāpu veidoÅ”anai. Ja paskatās uz to, kas atrodas krātuvē, kļūst nedaudz skumji: patiesÄ«bā tas tika pamests jau sen un liela daļa no tā ir ļoti novecojusi (mans bijuÅ”ais kolēģis savulaik tur veica vairākus labojumus, bet tas bija sen) . Kopumā es nolēmu atjaunot Å”o lietu: es dakÅ”u, izmetu to, ko neplānoju izmantot, pārcēlu projektu uz cmmake, iekļāva ā€œkarstāsā€ mikrofunkcijas, no steka noņēma lielus masÄ«vus (un mainÄ«ga garuma masÄ«vus, kas, godÄ«gi sakot, padara mani par ā€œbumbuā€), palaida profilētāju vēlreiz – un atklāja, ka aptuveni 40% laika tiek pavadÄ«ts fwrite...

Kas tad notiek ar fwrite?

Å ajā kodā fwrite (manā konkrētajā testa gadÄ«jumā: veidojot ielāpu starp tuvu 300 MB failiem, ievades dati pilnÄ«bā atrodas atmiņā) tiek izsaukts miljoniem reižu ar mazu bufera izmēru. AcÄ«mredzot Ŕī lieta palēnināsies, un tāpēc es gribētu kaut kā ietekmēt Å”o negodu. Pagaidām nav vēlmes ieviest dažāda veida datu avotus, asinhrono ievadi-izvadi, gribēju atrast vienkārŔāku risinājumu. Pirmā lieta, kas ienāca prātā, bija palielināt bufera izmēru

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

bet es nesaņēmu bÅ«tisku rezultāta uzlabojumu (tagad fwrite veidoja aptuveni 37% laika) — tas nozÄ«mē, ka joprojām nav runa par biežu datu ierakstīŔanu diskā. Apskatot fwrite ā€œzem pārsegaā€, jÅ«s varat redzēt, ka bloķēŔanas/atbloķēŔanas FILE struktÅ«ra notiek kaut kas lÄ«dzÄ«gs Å”im (pseidokods, visa analÄ«ze tika veikta programmā 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;
}

Pēc profilētāja domām, _fwrite_nolock veido tikai 6% laika, pārējais ir pieskaitāms. Manā konkrētajā gadÄ«jumā pavedienu droŔība ir nepārprotami pārmērÄ«ga, tāpēc es to ziedoÅ”u, aizstājot fwrite zvanu ar _fwrite_nolock - jums pat nav jābÅ«t gudram ar argumentiem. Kopā: Ŕī vienkārŔā manipulācija ievērojami samazināja rezultāta ierakstīŔanas izmaksas, kas sākotnējā versijā bija gandrÄ«z puse no pavadÄ«tā laika. Starp citu, POSIX pasaulē ir lÄ«dzÄ«ga funkcija - fwrite_unlocked. VispārÄ«gi runājot, tas pats attiecas uz fread. Tādējādi, izmantojot #defines pāri, jÅ«s varat iegÅ«t pilnÄ«gi starpplatformu risinājumu bez nevajadzÄ«gām slēdzenēm, ja tās nav vajadzÄ«gas (un tas notiek diezgan bieži).

fwrite, _fwrite_nolock, setvbuf

Attālināsimies no sākotnējā projekta un pievērsÄ«simies konkrēta gadÄ«juma testēŔanai: liela faila (512 MB) ierakstīŔana ārkārtÄ«gi mazos fragmentos — pa vienam baitam katrā. Testa sistēma: AMD Ryzen 7 1700, 16 GB RAM, 7200 apgr./min cietais disks, 64 MB keÅ”atmiņa. Windows 10 1809. gadā binārais fails tika veidots kā 32 bitu, optimizācijas ir iespējotas, bibliotēka ir statiski saistÄ«ta.

Eksperimenta paraugs:


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

MainÄ«gie lielumi bÅ«s TEST_BUFFER_SIZE, un dažos gadÄ«jumos mēs aizstāsim fwrite_unlocked ar fwrite. Sāksim ar fwrite gadÄ«jumu, nepārprotami iestatot bufera lielumu (komentējiet setvbuf un saistÄ«to kodu): laiks 27048906 µs, rakstīŔanas ātrums - 18.93 MB/s. Tagad iestatÄ«sim bufera izmēru uz 64 KB: laiks - 25037111 μs, ātrums - 20.44 Mb/s. Tagad pārbaudÄ«sim _fwrite_nolock darbÄ«bu, neizsaucot setvbuf: 7262221 µs, ātrums - 70.5 Mb/s!

Tālāk eksperimentēsim ar bufera lielumu (setvbuf):

Paātriniet C/C++ failu ievadi/izvadi, nezaudējot sviedrus

Dati tika iegÅ«ti, vidēji aprēķinot 5 eksperimentus; es biju pārāk slinks, lai aprēķinātu kļūdas. Kas attiecas uz mani, 93 MB/s, rakstot 1 baitu uz parastu HDD, ir ļoti labs rezultāts, tikai jāizvēlas optimālais bufera izmērs (manā gadÄ«jumā 256 KB ir tieÅ”i piemērots) un fwrite jāaizstāj ar _fwrite_nolock/fwrite_unlocked ( gadÄ«jumā, ja vÄ«tnes droŔība, protams, nav nepiecieÅ”ama).
Tāpat ar fread lÄ«dzÄ«gos apstākļos. Tā kā man nav pie rokas aparatÅ«ras maŔīnas ar Linux (viena borta datori netiek skaitÄ«ti), es nolēmu veikt ierobežotu eksperimentu ar virtuālo maŔīnu (Hyper-V, OpenSUSE 15, GCC 8.3.1) - modelis principā ir tas pats: ā€œpliksā€ fwrite 20 Mb/s, fwrite + 256 KB buferis ražots 23 Mb/s, fwrite_unlocked ar to paÅ”u buferi - 35 Mb/s (64 bitu binārs, samontēts g++ -o2 - s -static-libgcc -static-libstdc++ fwrite_test. cpp -o fwrite_test).

Pēcvārds

Å Ä« raksta rakstīŔanas mērÄ·is bija aprakstÄ«t vienkārÅ”u un daudzos gadÄ«jumos efektÄ«vu paņēmienu (es nekad iepriekÅ” neesmu saskāries ar _fwrite_nolock/fwrite_unlocked funkcijām, tās nav Ä«paÅ”i populāras - bet velti). Es neizliekos, ka materiāls ir jauns, bet ceru, ka raksts bÅ«s noderÄ«gs sabiedrÄ«bai.

Avots: www.habr.com

Iegādājieties uzticamu mitināŔanu vietnēm ar DDoS aizsardzÄ«bu, VPS VDS serveriem šŸ”„ Iegādājieties uzticamu tÄ«mekļa vietņu mitināŔanu ar DDoS aizsardzÄ«bu, VPS VDS serveriem | ProHoster