
Vorwort
Es gibt so ein einfaches und sehr nĂŒtzliches Dienstprogramm auf der Welt - , und so kam es, dass es schon sehr lange in unserem Produktionsprozess verankert war (obwohl es nicht möglich war, seine Version zu installieren, aber es war definitiv nicht die letzte verfĂŒgbare Version). Wir verwenden es fĂŒr den vorgesehenen Zweck â die Erstellung von BinĂ€r-Patches. Wenn man sich ansieht, was sich im Repository befindet, wird es ein wenig traurig: TatsĂ€chlich wurde es vor langer Zeit aufgegeben und vieles davon ist sehr veraltet (mein ehemaliger Kollege hat dort einmal mehrere Ănderungen vorgenommen, aber das ist lange her). . Im Allgemeinen habe ich beschlossen, diese Angelegenheit wiederzubeleben: Ich habe gespalten, weggeworfen, was ich nicht verwenden wollte, und das Projekt dorthin verschoben , integrierte âheiĂeâ Mikrofunktionen, entfernte groĂe Arrays vom Stapel (und Arrays variabler LĂ€nge, die mich, ehrlich gesagt, âzu einer Bombeâ machen), lieĂ den Profiler noch einmal laufen â und stellte fest, dass etwa 40 % der Zeit dafĂŒr aufgewendet wird ...
Was ist also mit fwrite los?
In diesem Code wird fwrite (in meinem speziellen Testfall: Erstellen eines Patches zwischen knapp 300 MB groĂen Dateien, die Eingabedaten liegen vollstĂ€ndig im Speicher) millionenfach mit einer kleinen PuffergröĂe aufgerufen. Offensichtlich wird sich diese Sache verlangsamen, und deshalb möchte ich diese Schande irgendwie beeinflussen. Es besteht noch kein Wunsch, verschiedene Arten von Datenquellen und asynchrone Eingabe-Ausgabe zu implementieren. Ich wollte eine einfachere Lösung finden. Das erste, was mir in den Sinn kam, war, die PuffergröĂe zu erhöhen
setvbuf(file, nullptr, _IOFBF, 64* 1024)Ich habe jedoch keine wesentliche Verbesserung des Ergebnisses erzielt (jetzt machte fwrite etwa 37 % der Zeit aus) â was bedeutet, dass es immer noch nicht darum geht, hĂ€ufig Daten auf die Festplatte zu schreiben. Wenn Sie âunter die Haubeâ von fwrite blicken, können Sie erkennen, dass in etwa dieser Dateistruktur eine Sperre/Entsperrung stattfindet (Pseudocode, alle Analysen wurden unter Visual Studio 2017 durchgefĂŒhrt):
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;
}
Laut Profiler macht _fwrite_nolock nur 6 % der Zeit aus, der Rest ist Overhead. In meinem speziellen Fall ist die Thread-Sicherheit eindeutig ĂŒbertrieben, daher werde ich sie opfern, indem ich den fwrite-Aufruf durch ersetze - Sie mĂŒssen nicht einmal geschickt mit Argumenten umgehen können. Insgesamt: Diese einfache Manipulation reduzierte den Aufwand fĂŒr die Ergebnisaufzeichnung erheblich, der in der Originalversion fast die HĂ€lfte des Zeitaufwands ausmachte. In der POSIX-Welt gibt es ĂŒbrigens eine Ă€hnliche Funktion - . Im Allgemeinen gilt das Gleiche auch fĂŒr Fread. Wenn Sie also ein Paar #defines verwenden, können Sie eine vollstĂ€ndig plattformĂŒbergreifende Lösung ohne unnötige Sperren erhalten, wenn diese nicht erforderlich sind (was hĂ€ufig vorkommt).
fwrite, _fwrite_nolock, setvbuf
Wir wenden uns vom ursprĂŒnglichen Projekt ab und konzentrieren uns auf einen konkreten Anwendungsfall: das Schreiben einer groĂen Datei (512 MB) in extrem kleinen Teilen â jeweils ein Byte. Testsystem: AMD Ryzen 7 1700, 16 GB RAM, 7200-U/min-Festplatte, 64 MB Cache. Windows 10 1809 wurde die BinĂ€rdatei als 32-Bit erstellt, Optimierungen sind aktiviert, die Bibliothek ist statisch verlinkt.
Beispiel fĂŒr das 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;
}
Die Variablen lauten TEST_BUFFER_SIZE und in einigen FĂ€llen ersetzen wir fwrite_unlocked durch fwrite. Beginnen wir mit dem fwrite-Fall, ohne die PuffergröĂe explizit festzulegen (setvbuf und den zugehörigen Code auskommentieren): Zeit 27048906 ”s, Schreibgeschwindigkeit â 18.93 MB/s. Stellen wir nun die PuffergröĂe auf 64 KB ein: Zeit â 25037111 ÎŒs, Geschwindigkeit â 20.44 Mbit/s. Testen wir nun die Funktionsweise von _fwrite_nolock, ohne setvbuf aufzurufen: 7262221 ”s, Geschwindigkeit â 70.5 Mbit/s!
Als nĂ€chstes experimentieren wir mit der PuffergröĂe (setvbuf):

Die Daten wurden durch Mittelung von 5 Experimenten ermittelt; ich war zu faul, die Fehler zu berechnen. FĂŒr mich sind 93 MB/s beim Schreiben von 1 Byte auf eine normale Festplatte ein sehr gutes Ergebnis. Sie mĂŒssen nur die optimale PuffergröĂe auswĂ€hlen (in meinem Fall sind 256 KB genau richtig) und fwrite durch _fwrite_nolock/fwrite_unlocked ersetzen ( fĂŒr den Fall, dass Thread-Sicherheit nicht benötigt wird, natĂŒrlich).
Ebenso mit Fread unter Ă€hnlichen Bedingungen. Da ich keine Hardware-Maschine mit Linux zur Hand habe (Einplatinencomputer zĂ€hlen nicht), habe ich beschlossen, ein begrenztes Experiment auf einer virtuellen Maschine (Hyper-V, OpenSUSE 15, GCC 8.3.1) durchzufĂŒhren â der Das Muster ist im Prinzip dasselbe: ânacktesâ fwrite 20 Mbit/s, fwrite + 256 KB Puffer erzeugt 23 Mbit/s, fwrite_unlocked mit demselben Puffer â 35 Mbit/s (64-Bit-BinĂ€rdatei, assembliertes G++ -o2 - s -static-libgcc -static-libstdc++ fwrite_test.
Nachwort
Der Zweck dieses Artikels bestand darin, eine einfache und in vielen FĂ€llen effektive Technik zu beschreiben (die Funktionen _fwrite_nolock/fwrite_unlocked sind mir noch nie begegnet, sie sind nicht sehr beliebt â aber vergebens). Ich behaupte nicht, dass das Material neu ist, aber ich hoffe, dass der Artikel fĂŒr die Community nĂŒtzlich sein wird.
Source: habr.com
