Более быстрый (небезопасный) BinaryReader в .NET
-
11-09-2019 - |
Вопрос
Я столкнулся с ситуацией, когда у меня есть довольно большой файл, из которого мне нужно прочитать двоичные данные.
В результате я понял, что реализация BinaryReader по умолчанию в .NET работает довольно медленно.Посмотрев на это с .NET-отражатель Я наткнулся на это:
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));
}
}
...
}
Это намного быстрее: мне удалось сократить время чтения файла размером 500 МБ на 5–7 секунд, но в целом это все равно довольно медленно (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;
}
Есть еще идеи, как сделать это еще быстрее?Я подумал, может быть, я мог бы использовать маршалинг, чтобы отобразить весь файл прямо в памяти поверх какой-то специальной структуры, поскольку данные являются линейными, фиксированного размера и последовательными.
РЕШЕНО: Я пришел к выводу, что буферизация/BufferedStream FileStream имеет недостатки.См. принятый ответ и мой собственный ответ (с решением) ниже.
Решение
Когда вы копируете файл, большие объемы данных считываются и записываются на диск.
Вы читаете весь файл по четыре байта за раз.Это обязательно будет медленнее.Даже если реализация потока достаточно умна для буферизации, у вас все равно будет как минимум 500 МБ/4 = 131072000 вызовов API.
Не разумнее ли просто прочитать большой фрагмент данных, а затем просмотреть его последовательно и повторять до тех пор, пока файл не будет обработан?
Другие советы
Я столкнулся с аналогичной проблемой производительности с 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
пример.
Интересно, что чтение всего файла в буфер и просмотр его в памяти имело огромное значение.Это за счет памяти, но ее у нас предостаточно.
Это заставляет меня думать, что реализация буфера 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.Что на данный момент достаточно хорошо.
Одно предостережение;возможно, вы захотите перепроверить свой Порядок байтов процессора...если предположить, что прямой порядок байтов не является довольно безопасно (подумайте:итан и др.).
Возможно, вы также захотите узнать, BufferedStream
имеет какое-то значение (я не уверен, что это будет).