
Vorwort
Es gibt ein solches einfaches und sehr nĂŒtzliches Tool in der Welt â , und es hat sich so ergeben, dass es schon lange in unserem Produktionsprozess verwurzelt ist (obwohl ich es nicht geschafft habe, die Version zu installieren, war sie definitiv nicht die neueste verfĂŒgbare). Wir verwenden es fĂŒr seinen ursprĂŒnglichen Zweck â das Erstellen von binĂ€ren Patches. Wenn man sieht, was im Repository ist, wird es ein wenig traurig: Es ist im Wesentlichen seit langem aufgegeben worden und vieles dort ist stark veraltet (vor langer Zeit hat ein ehemaliger Kollege einige Ănderungen vorgenommen, aber das ist lange her). Ich habe beschlossen, das Ganze wiederzubeleben: Ich habe geforkt, was ich nicht nutzen will, entfernt und das Projekt auf , âheiĂeâ Mikrofunktionen inline integriert, groĂe Arrays vom Stack entfernt (und Arrays variabler LĂ€nge, die mir offen gesagt âauf die Nerven gingenâ), den Profiler erneut durchlaufen lassen â und festgestellt, dass etwa 40 % der Zeit auf âŠ
verwendet werden. Was ist also mit fwrite?
Im vorliegenden Code wird fwrite (in meinem spezifischen Testfall: Patch-Erstellung zwischen nahen 300 MB Dateien, die Eingabedaten vollstĂ€ndig im Speicher) millionenfach mit einem kleinen Puffer aufgerufen. Es ist offensichtlich, dass diese Angelegenheit verlangsamen wird, und deshalb wollte ich irgendwie Einfluss darauf nehmen, ohne allzu groĂe Umstellungen vorzunehmen. Quellen fĂŒr Daten einfĂŒhren, asynchrone Ein- und Ausgabe ist momentan nicht gewĂŒnscht; ich wollte eine einfachere Lösung finden. Das Erste, was mir einfiel, war, die PufferspeichergröĂe zu erhöhen
setvbuf(file, nullptr, _IOFBF, 64* 1024), aber ich habe keine wesentliche Verbesserung des Ergebnisses erhalten (jetzt wurden etwa 37 % der Zeit auf fwrite verwendet) â das heiĂt, es liegt wohl nicht an der hĂ€ufigen SchreibvorgĂ€ngen auf die Festplatte. Ein Blick âunter die Haubeâ von fwrite zeigt, dass intern ungefĂ€hr so etwas wie lock/unlock der FILE-Struktur geschieht (Pseudocode, die gesamte Analyse erfolgte unter 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 *\/\n __try\n {\n retval = _fwrite_nolock(buffer, size, count, stream);\n }\n __finally \n {\n _unlock_str(stream); \/\* unlock stream *\/\n }\n return retval;\n}
Wenn der Profiler zu glauben ist, entfĂ€llt auf _fwrite_nolock lediglich 6 % der Zeit, der Rest ist Overhead. In meinem speziellen Fall ist Thread-Sicherheit ein offensichtlicher Luxus, den ich opfere, indem ich den Aufruf von fwrite durch â man muss nicht einmal mit Argumenten herumspielen. Insgesamt hat diese einfache Manipulation die Kosten fĂŒr das Ergebnis erheblich reduziert, die im ursprĂŒnglichen Fall fast die HĂ€lfte der zeitlichen Aufwendungen ausmachten. Ăbrigens gibt es in der Welt von POSIX eine Ă€hnliche Funktion â . GrundsĂ€tzlich gilt das Gleiche fĂŒr fread. Somit kann man mit einem Paar #define eine völlig plattformĂŒbergreifende Lösung ohne unnötige Sperren erhalten, sofern diese nicht notwendig sind (und das kommt ziemlich oft vor).
fwrite, _fwrite_nolock, setvbuf
Lassen Sie uns von dem ursprĂŒnglichen Projekt abstrahieren und einen spezifischen Fall testen: das Schreiben einer groĂen Datei (512 MB) in minimalen Portionen â jeweils 1 Byte. Testsystem: AMD Ryzen 7 1700, 16 GB RAM, HDD 7200 U/min mit 64 MB Cache, Windows 10 1809, das Binary wurde 32-Bit kompiliert, Optimierungen sind aktiviert, die Bibliothek wurde 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;
}
Als Variablen werden TEST_BUFFER_SIZE verwendet, auĂerdem ersetzen wir in einigen FĂ€llen fwrite_unlocked durch fwrite. Wir beginnen mit dem Fall fwrite ohne explizite PuffergröĂe (wir kommentieren setvbuf und den dazugehö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 _fwrite_nolock ohne Aufruf von setvbuf: 7262221 ”s, Geschwindigkeit â 70,5 MB/s!
Jetzt experimentieren wir mit der PuffergröĂe (setvbuf):

Die Daten stammen aus dem Durchschnitt von 5 Experimenten, die Fehler zu berechnen, war mir zu mĂŒhsam. Meiner Meinung nach ist 93 MB/s beim Schreiben von 1 Byte auf eine normale HDD ein sehr gutes Ergebnis; man muss lediglich die optimale PuffergröĂe wĂ€hlen (in meinem Fall 256 KB â das ist genau richtig) und fwrite durch _fwrite_nolock/fwrite_unlocked ersetzen (sofern keine Thread-Sicherheit erforderlich ist, versteht sich).
Ăhnlich verhĂ€lt es sich mit fread unter Ă€hnlichen Bedingungen. Da ich keinen "hardwaremĂ€Ăigen" Rechner mit Linux zur Hand hatte (Einplatinencomputer zĂ€hlen nicht), beschloss ich, ein begrenztes Experiment auf einer virtuellen Maschine (Hyper-V, OpenSUSE 15, GCC 8.3.1) durchzufĂŒhren â die RegelmĂ€Ăigkeit ist grundsĂ€tzlich die gleiche: ânacktesâ fwrite 20 MB/s, fwrite + Puffer von 256 KB gab 23 MB/s, fwrite_unlocked mit demselben Puffer â 35 MB/s (Binary 64-Bit, 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 hatte ich vorher nie zu tun, sie sind nicht besonders beliebt â zu Unrecht). Ich erhebe keinen Anspruch auf Neuheit des Materials, hoffe jedoch, dass der Artikel der Gemeinschaft nĂŒtzlich sein wird.
Quelle: habr.com
