Was passiert hinter den Kulissen, wenn ich nach einer Position in einer Datei suche und eine kleine Datenmenge (20 Bytes) schreibe?
Mein Verständnis
Meines Wissens ist die kleinste Dateneinheit, die von einer Festplatte geschrieben oder gelesen werden kann, ein Sektor (traditionell 512 Bytes, aber dieser Standard ändert sich jetzt). Das bedeutet, dass ich zum Schreiben von 20 Bytes einen ganzen Sektor lesen, einen Teil davon im Speicher ändern und zurück auf die Festplatte schreiben muss.
Dies erwarte ich bei ungepufferter E / A. Ich erwarte auch, dass gepufferte E/A ungefähr dasselbe tun, aber seien Sie klug mit seinem Cache. Also hätte ich gedacht, dass, wenn ich die Lokalität durch zufällige Suchvorgänge und Schreibvorgänge aus dem Fenster blase, sowohl gepufferte als auch ungepufferte E / A eine ähnliche Leistung haben sollten … vielleicht mit ungepufferter etwas besserer Leistung.
Andererseits weiß ich, dass es für gepufferte E/A verrückt ist, nur einen Sektor zu puffern, also könnte ich auch erwarten, dass es schrecklich funktioniert.
Meine Bewerbung
Ich speichere Werte, die von einem SCADA-Gerätetreiber gesammelt wurden, der Remote-Telemetrie für mehr als hunderttausend Punkte empfängt. Es gibt zusätzliche Daten in der Datei, sodass jeder Datensatz 40 Byte groß ist, aber nur 20 Byte davon müssen während einer Aktualisierung geschrieben werden.
Benchmark vor der Implementierung
Um zu überprüfen, ob ich mir keine brillant überentwickelte Lösung ausdenken muss, habe ich einen Test mit ein paar Millionen zufälligen Datensätzen durchgeführt, die in eine Datei geschrieben wurden, die insgesamt 200.000 Datensätze enthalten könnte. Bei jedem Test wird der Zufallszahlengenerator mit demselben Wert gesät, um fair zu sein. Zuerst lösche ich die Datei und fülle sie auf die Gesamtlänge (ca. 7,6 MB) auf, mache dann eine Schleife ein paar Millionen Mal und übergebe einen zufälligen Datei-Offset und einige Daten an eine von zwei Testfunktionen:
void WriteOldSchool( void *context, long offset, Data *data )
{
int fd = (int)context;
lseek( fd, offset, SEEK_SET );
write( fd, (void*)data, sizeof(Data) );
}
void WriteStandard( void *context, long offset, Data *data )
{
FILE *fp = (FILE*)context;
fseek( fp, offset, SEEK_SET );
fwrite( (void*)data, sizeof(Data), 1, fp );
fflush(fp);
}
Vielleicht keine Überraschungen?
Das OldSchool
Methode hat sich durchgesetzt – mit Abstand. Es war über 6-mal schneller (1,48 Millionen gegenüber 232000 Datensätzen pro Sekunde). Um sicherzustellen, dass ich nicht auf Hardware-Caching gestoßen bin, habe ich meine Datenbankgröße auf 20 Millionen Datensätze (Dateigröße von 763 MB) erweitert und die gleichen Ergebnisse erzielt.
Bevor Sie auf den offensichtlichen Anruf hinweisen fflush
, lassen Sie mich sagen, dass das Entfernen keine Wirkung hatte. Ich nehme an, das liegt daran, dass der Cache übergeben werden muss, wenn ich weit genug weg suche, was ich die meiste Zeit tue.
So was ist los?
Es scheint mir, dass die gepufferte E / A einen großen Teil der Datei lesen (und möglicherweise vollständig schreiben) muss, wenn ich versuche zu schreiben. Da ich seinen Cache kaum ausnutze, ist dies äußerst verschwenderisch.
Außerdem (und ich kenne die Details des Hardware-Cachings auf der Festplatte nicht), wenn die gepufferte E / A versucht, eine Reihe von Sektoren zu schreiben, wenn ich nur einen ändere, würde dies die Effektivität des Hardware-Cache verringern.
Gibt es da draußen Festplattenexperten, die das besser kommentieren und erklären können als meine experimentellen Ergebnisse? =)
Ich gehe davon aus, dass fseek einen Flush verursacht (daher der fehlende Unterschied), da der Puffer geleert werden muss, bevor er verschoben werden kann.
– dave
1. November 2012 um 5:21 Uhr
Das Lesen und Schreiben hat die vom Dateisystem definierte Blockgröße. Unter Linux ext4 ist der Standardwert 4 KB. In ähnlicher Weise funktioniert der Dateicache (gepufferte Dateien) auf der Einheit PAGE_SIZE, wiederum höchstwahrscheinlich 4 KB.
– Rohan
1. November 2012 um 5:34 Uhr
Sie können die Pufferung auf Stdio-Ebene deaktivieren, indem Sie aufrufen
setbuf(fp, NULL);
nach dem Öffnenfp
.– Café
1. November 2012 um 6:45 Uhr
Abgesehen davon ist sizeof(Data) die Größe des Zeigers, nicht die Größe der Daten.
– janeb
1. November 2012 um 7:36 Uhr
@janneb Hoppla, das wurde eigentlich als C++-Projekt kompiliert, aber da es sich im Wesentlichen um C-Funktionen handelt, habe ich es als solches gekennzeichnet. Ich habe die C++-Syntax für die sizeof verwendet.
– Reisfeld
1. November 2012 um 8:38 Uhr