Более быстрый (небезопасный) BinaryReader в .NET - PullRequest
24 голосов
/ 06 августа 2009

Я столкнулся с ситуацией, когда у меня есть довольно большой файл, из которого мне нужно прочитать двоичные данные.

Следовательно, я понял, что реализация BinaryReader по умолчанию в .NET довольно медленная. Посмотрев на него с помощью .NET Reflector я наткнулся на это:

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));
}

Это кажется мне крайне неэффективным, если подумать о том, как компьютеры были разработаны для работы с 32-разрядными значениями с момента изобретения 32-разрядного ЦП.

Поэтому я создал собственный (небезопасный) класс FastBinaryReader с таким кодом:

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));
        }
    }
...
}

Что гораздо быстрее - мне удалось сэкономить 5-7 секунд за время, необходимое для чтения файла размером 500 МБ, но в целом это все еще довольно медленно (первоначально 29 секунд и ~ 22 секунды с моим FastBinaryReader) .

Это все еще немного сбивает меня с толку относительно того, почему все еще требуется так много времени, чтобы прочитать такой относительно небольшой файл. Если я скопирую файл с одного диска на другой, это займет всего пару секунд, поэтому пропускная способность диска не является проблемой.

Я также добавил вызовы ReadInt32 и т. Д., И у меня получился следующий код:

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;
   }

Есть еще идеи, как сделать это еще быстрее? Я подумал, может быть, я мог бы использовать маршаллинг для отображения всего файла прямо в память поверх какой-либо пользовательской структуры, поскольку данные являются линейными, фиксированного размера и последовательными.

РЕШЕНО: Я пришел к выводу, что буферизация FileStream / BufferedStream несовершенна. Пожалуйста, посмотрите принятый ответ и мой собственный ответ (с решением) ниже.

Ответы [ 4 ]

18 голосов
/ 27 апреля 2012

Я столкнулся с аналогичной проблемой производительности в BinaryReader / FileStream, и после профилирования обнаружил, что проблема не в буферизации FileStream, а в следующей строке:

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

В частности, свойство br.BaseStream.Length для FileStream делает (относительно) медленный системный вызов для получения размера файла в каждом цикле. После изменения кода на это:

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

и используя соответствующий размер буфера для FileStream, я достиг производительности, аналогичной примеру MemoryStream.

10 голосов
/ 06 августа 2009

Когда вы выполняете копирование файлов, большие куски данных считываются и записываются на диск.

Вы читаете весь файл по четыре байта за раз. Это должно быть медленнее. Даже если потоковая реализация достаточно умна для буферизации, у вас все равно есть как минимум 500 МБ / 4 = 131072000 вызовов API.

Разве не разумнее просто прочитать большой кусок данных, а затем последовательно просмотреть его и повторять, пока файл не будет обработан?

10 голосов
/ 06 августа 2009

Интересно, что чтение всего файла в буфер и просмотр его в памяти имели огромное значение. Это за счет памяти, но у нас есть много.

Это заставляет меня думать, что реализация буфера FileStream (или BufferedStream в этом отношении) ошибочна, потому что независимо от того, какой размер буфера я пробовал, производительность все равно не оправдалась. 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; } } } Понижено до 2-5 секунд (зависит от дискового кэша, который я предполагаю), теперь с 22. Что сейчас достаточно.

5 голосов
/ 06 августа 2009

одно предостережение; Возможно, вы захотите перепроверить свой порядковый номер процессора ... при условии, что little-endian не является вполне безопасным (подумайте: itanium и т. д.).

Возможно, вы также захотите узнать, если BufferedStream что-то изменит (я не уверен, что так и будет).

...