
Prefaci
Hi ha una utilitat tan senzilla i molt útil al món: , i va passar que feia molt de temps que estava arrelat al nostre procés de producció (tot i que no va ser possible instal·lar-ne la versió, però definitivament no era l'última disponible). L'utilitzem per al propòsit previst: crear pedaços binaris. Si mireu el que hi ha al repositori, es fa una mica trist: de fet, fa molt de temps que es va abandonar i bona part està molt desfasat (el meu antic company hi va fer diverses modificacions una vegada, però això va ser fa molt de temps) . En general, vaig decidir ressuscitar aquest assumpte: vaig bifurcar, vaig llençar allò que no pensava utilitzar, vaig traslladar el projecte a , microfuncions "calentes" en línia, van eliminar grans matrius de la pila (i matrius de longitud variable, que francament em fan "bombarda"), van tornar a executar el perfilador i van descobrir que aproximadament el 40% del temps es dedica a ...
Aleshores, què passa amb fwrite?
En aquest codi, fwrite (en el meu cas de prova específic: construir un pedaç entre fitxers propers a 300 MB, les dades d'entrada estan completament a la memòria) s'anomena milions de vegades amb una mida de memòria intermèdia petita. Òbviament, això s'alentirà i, per tant, m'agradaria influir d'alguna manera en aquesta desgràcia. Encara no hi ha cap voluntat d'implementar diversos tipus de fonts de dades, E/S asíncrones, volia trobar una solució més senzilla. El primer que em va venir al cap va ser augmentar la mida del buffer
setvbuf(file, nullptr, _IOFBF, 64* 1024)però no vaig obtenir una millora significativa en el resultat (ara fwrite representava al voltant del 37% del temps), cosa que significa que encara no es tracta d'escriure dades amb freqüència al disc. Mirant "sota el capó" de fwrite, podeu veure que s'està produint una estructura de FITXER de bloqueig/desbloqueig dins d'alguna cosa com aquesta (pseudocodi, totes les anàlisis es van dur a terme sota 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;
}
Segons el perfilador, _fwrite_nolock només representa el 6% del temps, la resta és per sobre. En el meu cas particular, la seguretat del fil és clarament excessiva, així que la sacrificaré substituint la trucada fwrite per - ni tan sols cal ser intel·ligent amb els arguments. Total: aquesta senzilla manipulació va reduir significativament el cost de gravar el resultat, que en la versió original representava gairebé la meitat del temps dedicat. Per cert, al món POSIX hi ha una funció similar: . En termes generals, el mateix s'aplica al fread. Així, fent servir un parell de #defines, podeu obtenir una solució completament multiplataforma sense bloquejos innecessaris si no són necessaris (i això passa amb força freqüència).
fwrite, _fwrite_nolock, setvbuf
Allunyem-nos del projecte original i centrem-nos en provar un cas específic: escriure un fitxer gran (512 MB) en fragments extremadament petits, d'un byte cadascun. Sistema de prova: AMD Ryzen 7 1700, 16 GB de RAM, disc dur de 7200 rpm, 64 MB de memòria cau. Windows 10 1809, el binari es va construir com a 32 bits, les optimitzacions estan habilitades i la biblioteca està enllaçada estàticament.
Mostra per a l'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;
}
Les variables seran TEST_BUFFER_SIZE, i durant un parell de casos substituirem fwrite_unlocked per fwrite. Comencem amb el cas fwrite sense establir explícitament la mida del buffer (comenta setvbuf i el codi associat): temps 27048906 µs, velocitat d'escriptura - 18.93 MB/s. Ara establim la mida del buffer a 64 KB: temps - 25037111 μs, velocitat - 20.44 Mb/s. Ara provem el funcionament de _fwrite_nolock sense cridar a setvbuf: 7262221 µs, velocitat - 70.5 Mb/s!
A continuació, experimentem amb la mida del buffer (setvbuf):

Les dades es van obtenir fent una mitjana de 5 experiments; vaig ser massa mandrós per calcular els errors. Pel que fa a mi, 93 MB/s quan escriu 1 byte en un HDD normal és un molt bon resultat, només cal que seleccioneu la mida òptima de memòria intermèdia (en el meu cas, 256 KB és el correcte) i substituïu fwrite per _fwrite_nolock/fwrite_unlocked ( per si no cal la seguretat del fil, és clar).
Igualment amb fread en condicions similars. Com que no tinc una màquina de maquinari amb Linux a mà (els ordinadors de placa única no compten), vaig decidir dur a terme un experiment limitat en una màquina virtual (Hyper-V, OpenSUSE 15, GCC 8.3.1) - el El patró és bàsicament el mateix: "naked" fwrite 20 Mb/s, fwrite + 256 KB buffer produït 23 Mb/s, fwrite_unlocked amb el mateix buffer - 35 Mb/s (binari de 64 bits, muntat g++ -o2 -s -static -libgcc -static-libstdc++ fwrite_test. cpp -o fwrite_test).
Paraula posterior
L'objectiu d'escriure aquest article era descriure una tècnica senzilla i eficaç en molts casos (mai abans m'havia trobat amb les funcions _fwrite_nolock/fwrite_unlocked, no són molt populars, però en va). No pretenc que el material sigui nou, però espero que l'article sigui útil a la comunitat.
Font: www.habr.com
