
Vorwort
Es gibt ein so einfaches und sehr nützliches Tool auf der Welt — , und es stellt sich heraus, dass es schon lange fest in unseren Produktionsprozess integriert ist (obwohl es mir nicht gelungen ist, die Version zu installieren, war sie definitiv nicht die neueste verfügbare). Wir nutzen es entsprechend seinem Zweck – zur Erstellung binärer Patches. Wenn man sich anschaut, was im Repository ist, wird man etwas traurig: Es ist im Grunde schon lange aufgegeben und vieles dort ist stark veraltet (einst hat mein ehemaliger Kollege einige Änderungen vorgenommen, aber das ist lange her). Insgesamt habe ich beschlossen, dieses Projekt wiederzubeleben: ich habe einen Fork erstellt, das, was ich nicht nutzen möchte, entfernt, das Projekt auf , die „heißen“ Mikrofunktionen inline gesetzt, große Arrays (und variabel große Arrays, von denen ich ehrlich gesagt „beleidigt“ bin) aus dem Stack entfernt, und erneut den Profiler durchlaufen lassen — und festgestellt, dass etwa 40 % der Zeit auf …
Was ist also mit fwrite?
In diesem Code wird fwrite (in meinem speziellen Testfall: Patch-Bau zwischen zwei etwa 300 MB großen Dateien, die Eingaben sind vollständig im Speicher) millionenfach mit einem kleinen Puffer aufgerufen. Offensichtlich wird das Ganze dadurch verlangsamt, und daher möchte ich auf irgendeine Weise Einfluss auf dieses Problem nehmen. Datenquellen und asynchrones Ein- und Ausgabeverfahren einzuführen, ist momentan nicht mein Wunsch; 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)Aber ich habe keine wesentlichen Verbesserungen erzielt (jetzt verbrachte fwrite etwa 37 % der Zeit) — das bedeutet, dass es nicht nur an häufigen Schreibvorgängen auf die Festplatte liegt. Wenn man „unter die Haube“ von fwrite schaut, sieht man, dass innerhalb der Funktion ca. so ein lock/unlock der FILE-Struktur erfolgt (Pseudocode, die gesamte Analyse wurde 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); /* Stream sperren */
__try
{
retval = _fwrite_nolock(buffer, size, count, stream);
}
__finally
{
_unlock_str(stream); /* Stream entsperren */
}
return retval;
}
Wenn man dem Profiler Glauben schenken darf, entfallen auf _fwrite_nolock nur 6% der Zeit, der Rest ist Overhead. In meinem speziellen Fall ist Thread-Sicherheit ein offensichtlicher Luxus, den ich opfern werde, indem ich den Aufruf von fwrite durch ersetze — selbst mit den Argumenten muss man nicht herumtüfteln. Zusammengefasst: Diese einfache Manipulation hat die Kosten für das Schreiben des Ergebnisses um ein Vielfaches gesenkt, die in der ursprünglichen Version fast die Hälfte der Zeitkosten ausmachten. Übrigens gibt es in der POSIX-Welt eine ähnliche Funktion — . Im Allgemeinen gilt dies auch für fread. Mit ein paar #define kann man somit eine voll funktionsfähige plattformübergreifende Lösung ohne unnötige Sperren erhalten, wenn sie nicht notwendig sind (was ziemlich oft der Fall ist).
fwrite, _fwrite_nolock, setvbuf
Lassen Sie uns vom ursprünglichen Projekt abstrahieren und uns mit der spezifischen Testung eines Falls befassen: das Schreiben einer großen Datei (512 MB) in extrem kleinen Portionen — zu 1 Byte. Testsystem: AMD Ryzen 7 1700, 16 GB RAM, HDD 7200 rpm 64 MB Cache, Windows 10 1809, 32-Bit-Binärdatei, Optimierungen aktiviert, Bibliothek statisch verlinkt.
Testbeispiel 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 werden TEST_BUFFER_SIZE sein, und in einigen Fällen ersetzen wir fwrite_unlocked durch fwrite. Beginnen wir mit dem Fall von fwrite ohne explizite Puffergröße (wir kommentieren setvbuf und den zugehörigen Code aus): Zeit 27048906 µs, Schreibgeschwindigkeit — 18,93 MB/s. Jetzt setzen wir die Puffergröße auf 64 KB: Zeit — 25037111 µs, Geschwindigkeit — 20,44 MB/s. Jetzt testen wir die Funktion von _fwrite_nolock ohne den Aufruf von setvbuf: 7262221 µs, Geschwindigkeit — 70,5 MB/s!
Experimente mit der Puffergröße (setvbuf):

Die Daten wurden durch die Durchschnittswerte aus 5 Experimenten erhalten, ich habe es nicht für nötig gehalten, die Fehler zu berechnen. Meiner Meinung nach ist 93 MB/s beim Schreiben von 1 Byte auf eine normale HDD ein sehr gutes Ergebnis; man muss nur die optimale Puffergröße wählen (in meinem Fall 256 KB — genau richtig) und fwrite durch _fwrite_nolock/fwrite_unlocked ersetzen (es sei denn, thread-sichere Sicherheit ist nötig, versteht sich).
Ähnlich wie bei fread in solchen Bedingungen. Da ich keine "echte" Maschine mit Linux zur Verfügung habe (Einplatinencomputer zählen nicht), habe ich beschlossen, ein begrenztes Experiment auf einer virtuellen Maschine durchzuführen (Hyper-V, OpenSUSE 15, GCC 8.3.1) — die Regelmäßigkeit ist im Grunde die gleiche: "nacktes" fwrite 20 MB/s, fwrite + Puffer von 256 KB ergab 23 MB/s, fwrite_unlocked mit demselben Puffer — 35 MB/s (64-Bit-Binärdatei, kompiliert mit g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test).
Nachwort
Ziel dieses Artikels war es, eine einfache und in vielen Fällen effektive Methode zu beschreiben (mit den Funktionen _fwrite_nolock/fwrite_unlocked bin ich bisher nicht in Kontakt gekommen, sie sind nicht sehr populär — zu Unrecht). Ich beanspruche keine Neuheit des Materials, hoffe jedoch, dass der Artikel der Community nützlich sein wird.
Quelle: habr.com
