
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
