2009-08-06 4 views
22

Ich bin auf eine Situation gestoßen, in der ich eine ziemlich große Datei habe, von der ich binäre Daten lesen muss.Schneller (unsicherer) BinaryReader in .NET

Folglich erkannte ich, dass die binäre Standardreader Implementierung in .NET ziemlich langsam ist. Beim Blick auf sie mit .NET Reflector stieß ich auf diese:

public virtual int ReadInt32() 
{ 
    if (this.m_isMemoryStream) 
    { 
     MemoryStream stream = this.m_stream as MemoryStream; 
     return stream.InternalReadInt32(); 
    } 
    this.FillBuffer(4); 
    return (((this.m_buffer[0] | (this.m_buffer[1] << 8)) | (this.m_buffer[2] << 0x10)) | (this.m_buffer[3] << 0x18)); 
} 

Was mich als äußerst ineffizient schlägt, denken, wie Computer wurden entwickelt, um mit 32-Bit-Werten zu arbeiten, da die 32-Bit-CPU erfunden wurde.

Also machte ich meine eigene (unsicher) FastBinaryReader Klasse mit Code wie diese statt:

public unsafe class FastBinaryReader :IDisposable 
{ 
    private static byte[] buffer = new byte[50]; 
    //private Stream baseStream; 

    public Stream BaseStream { get; private set; } 
    public FastBinaryReader(Stream input) 
    { 
     BaseStream = input; 
    } 


    public int ReadInt32() 
    { 
     BaseStream.Read(buffer, 0, 4); 

     fixed (byte* numRef = &(buffer[0])) 
     { 
      return *(((int*)numRef)); 
     } 
    } 
... 
} 

die viel schneller ist - ich schaffte es 5-7 Sekunden hinter der Zeit zu rasieren es zu lesen, nahm 500   MB-Datei, aber es ist immer noch ziemlich langsam insgesamt (29 Sekunden zunächst und ~ 22 Sekunden jetzt mit meiner FastBinaryReader).

Es verwirrt mich immer noch, warum es immer noch so lange dauert, so eine relativ kleine Datei zu lesen. Wenn ich die Datei von einem Datenträger auf einen anderen kopiere, dauert es nur ein paar Sekunden, so dass der Datenträgerdurchsatz kein Problem darstellt.

ich inlined weiter die ReadInt32 usw. Anrufe, und ich endete mit diesem Code auf:

using (var br = new FastBinaryReader(new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 0x10000, FileOptions.SequentialScan))) 

    while (br.BaseStream.Position < br.BaseStream.Length) 
    { 
     var doc = DocumentData.Deserialize(br); 
     docData[doc.InternalId] = doc; 
    } 
} 

public static DocumentData Deserialize(FastBinaryReader reader) 
    { 
     byte[] buffer = new byte[4 + 4 + 8 + 4 + 4 + 1 + 4]; 
     reader.BaseStream.Read(buffer, 0, buffer.Length); 

     DocumentData data = new DocumentData(); 
     fixed (byte* numRef = &(buffer[0])) 
     { 
      data.InternalId = *((int*)&(numRef[0])); 
      data.b = *((int*)&(numRef[4])); 
      data.c = *((long*)&(numRef[8])); 
      data.d = *((float*)&(numRef[16])); 
      data.e = *((float*)&(numRef[20])); 
      data.f = numRef[24]; 
      data.g = *((int*)&(numRef[25])); 
     } 
     return data; 
    } 

weitere Ideen, wie dies noch schneller zu machen? Ich dachte, vielleicht könnte ich Marshalling verwenden, um die gesamte Datei direkt in den Speicher über einer benutzerdefinierten Struktur abzubilden, da die Daten linear, fest und sequentiell sind.

Gelöst: Ich kam zu dem Schluss, dass FileStream Puffer/BufferedStream fehlerhaft sind. Bitte beachten Sie die akzeptierte Antwort und meine eigene Antwort (mit der Lösung) unten.

+0

Es kann hilfreich sein: http://stackoverflow.com/questions/19558435/what-is-the-best-buffer-size-when-using-binaryreader-to-read-big-files-1gb/19837238? noredirect = 1 # 19837238 –

Antwort

9

Wenn Sie eine Dateikopie durchführen, werden große Datenblöcke gelesen und auf die Festplatte geschrieben.

Sie lesen die gesamte Datei vier Bytes gleichzeitig. Dies wird zwangsläufig langsamer sein. Auch wenn die Stream-Implementierung intelligent genug ist, um zu puffern, haben Sie immer noch mindestens 500   MB/4 = 131072000 API-Aufrufe.

Ist es nicht sinnvoller, nur einen großen Teil der Daten zu lesen und dann nacheinander durchzugehen und zu wiederholen, bis die Datei verarbeitet wurde?

+1

Es gibt einen Parameter im FileStream-Konstruktor, der die Puffergröße angibt, so dass das Lesen tatsächlich in Chunks erfolgt. Ich habe verschiedene Werte für die Puffergröße ausprobiert, aber es gab keine wesentlichen Verbesserungen. Besonders große Puffergrößen beeinträchtigten die Leistung meiner Messungen. – andreialecu

+0

noch tun Sie die immense Anzahl von Aufrufen zu 'ReadInt32'. Es ist viel schneller, es aus einem fortlaufenden Speicher zu holen. – Toad

+0

Bitte lesen Sie die Frage erneut, ich verwende nicht ReadInt32 in der tatsächlichen Implementierung, es gibt nur 1 gelesen pro Objekt, und alle Konvertierungen sind inline, siehe die letzten zwei Blöcke des Codes. – andreialecu

5

Ein Vorbehalt; Vielleicht möchten Sie Ihre CPU's endianness überprüfen ... unter der Annahme, Little-Endian ist nicht durchaus sicher (think: Itanium etc).

Sie könnten auch sehen, ob BufferedStream einen Unterschied macht (ich bin mir nicht sicher, ob es).

+0

Yup, ich bin mir bewusst, Endlichkeitsprobleme, aber dies ist eine proprietäre Anwendung, wo ich die volle Kontrolle über die Bereitstellung habe. Bezüglich BufferedStream ist der FileStream meiner Meinung nach bereits gepuffert, sodass nur ein unnötiger Zwischenpuffer hinzugefügt wird. P.S .: Ich benutze auch Ihre Protobuf-Bibliothek in diesem Projekt, vielen Dank dafür :) – andreialecu

+3

Ich habe gerade eine neue Messung mit einem Wrapping BufferedStream gemacht, und wie erwartet gibt es keinen Unterschied. – andreialecu

9

Interessant, die ganze Datei in einen Puffer zu lesen und sie im Speicher zu durchlaufen, machte einen großen Unterschied. Dies ist auf Kosten der Erinnerung, aber wir haben viel.

Das lässt mich denken, dass die Implementierung des Pufferspeichers von FileStream (oder BufferedStream) fehlerhaft ist, denn egal, welche Puffergröße ich probierte, die Leistung war immer noch schlecht.

using (var br = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 0x10000, FileOptions.SequentialScan)) 
    { 
     byte[] buffer = new byte[br.Length]; 
     br.Read(buffer, 0, buffer.Length); 
     using (var memoryStream = new MemoryStream(buffer)) 
     { 
      while (memoryStream.Position < memoryStream.Length) 
      { 
       var doc = DocumentData.Deserialize(memoryStream); 
       docData[doc.InternalId] = doc; 
      } 
     } 
    } 

unten auf 2-5 Sekunden (abhängig von der Disk-Cache vermute ich) jetzt von 22. Welche jetzt gut genug ist.

+0

so war meine Antwort nicht so fehlerhaft; ^) – Toad

+3

Danke. Aber es gibt tatsächlich ein Problem mit der Buffer-Implementierung von .NET, weil ich eine Puffergröße versucht habe, die genau so groß ist wie die Datei (was eigentlich dem zwischengeschalteten MemoryStream hätte entsprechen müssen), und das immer noch Performance-mäßig war. Theoretisch hätte Ihr Vorschlag überflüssig sein sollen, aber in der Praxis - Jackpot. – andreialecu

+6

können Sie einfach sagen var buffer = File.ReadAllBytes (cacheFilePath); Speichern Sie etwas Code und es ist viel schneller – gjvdkamp

16

ich in eine ähnliche Performance-Problem lief mit Binary/Filestream und nach Profilierung, entdeckte ich, dass das Problem nicht mit FileStream Pufferung ist, sondern mit dieser Zeile:

while (br.BaseStream.Position < br.BaseStream.Length) { 

Insbesondere die Eigenschaft br.BaseStream.Length auf einem macht einen (relativ) langsamen Systemaufruf, um die Dateigröße auf jeder Schleife zu erhalten. Nach der Änderung des Codes dies:

long length = br.BaseStream.Length; 
while (br.BaseStream.Position < length) { 

und unter Verwendung einer geeignete Puffergröße für die FileStream erreichte ich eine ähnliche Leistung auf das MemoryStream Beispiel.

Verwandte Themen