Mais rápido (inseguro) BinaryReader em .NET
-
11-09-2019 - |
Pergunta
me deparei com uma situação onde eu tenho um muito grande arquivo que eu preciso ler dados binários a partir de.
Por isso, eu percebi que a implementação BinaryReader padrão no .NET é bastante lento. Ao olhar para ele com .NET Reflector me deparei com esta:
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));
}
O que me parece extremamente ineficiente, pensando em como os computadores foram projetados para trabalhar com valores de 32 bits desde a CPU 32 bit foi inventado.
Então eu fiz minha própria classe FastBinaryReader (inseguro) com um código como este em vez disso:
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));
}
}
...
}
O que é muito mais rápido -. Eu consegui raspar 5-7 segundos do tempo que levou para ler um arquivo de 500 MB, mas ainda é bastante lento em geral (29 segundos inicialmente e ~ 22 segundos agora com minha FastBinaryReader
)
Ainda tipo de defletores me a respeito de porque ele ainda tem muito tempo para ler esse arquivo relativamente pequeno. Se eu copiar o arquivo a partir de um disco para outro leva apenas um par de segundos, de modo rígido rendimento não é um problema.
Eu ainda inlined a ReadInt32, etc. chamadas, e acabei com este código:
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;
}
Quaisquer outras idéias sobre como tornar isso ainda mais rápido? Eu estava pensando que talvez eu poderia usar triagem para mapear todo o arquivo diretamente para a memória em cima de uma estrutura personalizada, desde que os dados é linear, tamanho e seqüencial fixo.
RESOLVIDO: cheguei à conclusão de que o buffer de FileStream / BufferedStream são falhos. Por favor, veja a resposta aceita e minha própria resposta (com a solução) abaixo.
Solução
Quando você faz uma FileCopy, grandes blocos de dados são lidos e gravados no disco.
Você está lendo o arquivo inteiro de quatro bytes de cada vez. Este é obrigado a ser mais lento. Ainda que a implementação corrente é inteligente o suficiente para o buffer, você ainda tem pelo menos 500 MB / 4 = 131072000 chamadas de API.
Não é mais sensato para apenas ler um pedaço grande de dados e, em seguida, passar por isso sequencialmente, e repita até que o arquivo foi processado?
Outras dicas
Eu corri para um problema de desempenho semelhante com BinaryReader / FileStream, e depois de profiling, descobri que o problema não é com o buffer FileStream
, mas em vez disso, com esta linha:
while (br.BaseStream.Position < br.BaseStream.Length) {
Especificamente, a propriedade br.BaseStream.Length
em um FileStream
faz uma (relativamente) chamada de sistema lento para obter o tamanho do arquivo em cada loop. Depois de mudar o código para isso:
long length = br.BaseStream.Length;
while (br.BaseStream.Position < length) {
e usando um tamanho de tampão apropriado para o FileStream
, I conseguido um desempenho semelhante ao exemplo MemoryStream
.
Interessante, lendo o arquivo inteiro em um buffer e passar por isso na memória fez uma enorme diferença. Isso é com o custo de memória, mas nós temos a abundância.
Isto faz-me pensar que o FileStream (ou BufferedStream de para que o assunto) tampão implementação é falho, porque não importa o tamanho do buffer eu tentei, o desempenho ainda sugado.
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;
}
}
}
Para baixo para 2-5 segundos (depende do cache de disco eu estou supondo) agora de 22. O que é bom o suficiente para agora.
Uma ressalva; você pode querer verificar novamente o seu da CPU endianness ... assumindo little-endian não é muito segura (pense: itanium etc).
Você também pode querer ver se BufferedStream
faz alguma diferença (eu não tenho certeza que ele vai).