Эффективное чтение структурированных двоичных данных из файла - PullRequest
3 голосов
/ 20 января 2020

У меня есть следующий фрагмент кода, который читает двоичный файл и проверяет его:

 FileStream f = File.OpenRead("File.bin");
 MemoryStream memStream = new MemoryStream();
 memStream.SetLength(f.Length);
 f.Read(memStream.GetBuffer(), 0, (int)f.Length);
 f.Seek(0, SeekOrigin.Begin);
 var r = new BinaryReader(f);
 Single prevVal=0;
 do
 {
    r.ReadUInt32();
    var val = r.ReadSingle();
    if (prevVal!=0) {
       var diff = Math.Abs(val - prevVal) / prevVal;
       if (diff > 0.25)
          Console.WriteLine("Bad!");
    }
    prevVal = val;
 }
 while (f.Position < f.Length);

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

struct S{
   int a;
   float b;
}

Как бы я сделал это в C#?

Ответы [ 4 ]

3 голосов
/ 20 января 2020

определяет struct (возможно readonly struct) с явным макетом ([StructLayout(LayoutKind.Explicit)]), который точно совпадает с вашим кодом C ++, затем один из:

  1. откройте файл как файл с отображением в памяти, получить указатель на данные; используйте код unsafe для необработанного указателя или Unsafe.AsRef<YourStruct> для данных, и Unsafe.Add<> для итерации
  2. , откройте файл как файл с отображением в памяти, получите указатель на данные; создайте пользовательскую память над указателем (вашего T) и выполните итерации по диапазону
  3. , откройте файл как byte[]; создайте Span<byte> поверх byte[], затем используйте MemoryMarshal.Cast<,> для создания Span<YourType> и итерируйте по этому
  4. , чтобы открыть файл как byte[]; используйте fixed, чтобы закрепить byte* и получить указатель; используйте код unsafe для обхода указателя
  5. что-то включает в себя "конвейеры" - Pipe, который является буфером, возможно, используйте StreamConnection на FileStream для заполнения канала и рабочий l oop, который извлекается из трубы; осложнение: буферы могут быть несмежными и могут разбиваться в неудобных местах; решаемый, но тонкий код требуется всякий раз, когда первый интервал не меньше 8 байт , Четвёртое простое, но для очень больших данных вы, вероятно, предпочтете файлы с отображением в памяти
1 голос
/ 20 января 2020

Спасибо всем за очень полезные комментарии и ответы. Учитывая эти данные, это мое предпочтительное решение:

      [StructLayout(LayoutKind.Sequential, Pack = 1)]
      struct Data
      {
         public UInt32 dummy;
         public Single val;
      };
      static void Main(string[] args)
      {
         byte [] byteArray = File.ReadAllBytes("File.bin");
         ReadOnlySpan<Data> dataArray = MemoryMarshal.Cast<byte, Data>(new ReadOnlySpan<byte>(byteArray));
         Single prevVal=0;
         foreach( var v in dataArray) {
            if (prevVal!=0) {
               var diff = Math.Abs(v.val - prevVal) / prevVal;
               if (diff > 0.25)
                  Console.WriteLine("Bad!");
            }
            prevVal = v.val;
         }
      }
   }

Это действительно работает намного быстрее, чем оригинальная реализация.

1 голос
/ 20 января 2020

Это то, что мы используем (совместимо со старыми версиями C#):

public static T[] FastRead<T>(FileStream fs, int count) where T: struct
{
    int sizeOfT = Marshal.SizeOf(typeof(T));

    long bytesRemaining  = fs.Length - fs.Position;
    long wantedBytes     = count * sizeOfT;
    long bytesAvailable  = Math.Min(bytesRemaining, wantedBytes);
    long availableValues = bytesAvailable / sizeOfT;
    long bytesToRead     = (availableValues * sizeOfT);

    if ((bytesRemaining < wantedBytes) && ((bytesRemaining - bytesToRead) > 0))
    {
        Debug.WriteLine("Requested data exceeds available data and partial data remains in the file.");
    }

    T[] result = new T[availableValues];

    GCHandle gcHandle = GCHandle.Alloc(result, GCHandleType.Pinned);

    try
    {
        uint bytesRead;

        if (!ReadFile(fs.SafeFileHandle, gcHandle.AddrOfPinnedObject(), (uint)bytesToRead, out bytesRead, IntPtr.Zero))
        {
            throw new IOException("Unable to read file.", new Win32Exception(Marshal.GetLastWin32Error()));
        }

        Debug.Assert(bytesRead == bytesToRead);
    }

    finally
    {
        gcHandle.Free();
    }

    GC.KeepAlive(fs);

    return result;
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1415:DeclarePInvokesCorrectly")]
[DllImport("kernel32.dll", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]

private static extern bool ReadFile
(
    SafeFileHandle       hFile,
    IntPtr               lpBuffer,
    uint                 nNumberOfBytesToRead,
    out uint             lpNumberOfBytesRead,
    IntPtr               lpOverlapped
);

ПРИМЕЧАНИЕ. Конечно, это работает только для структур, которые содержат только блитируемые типы. И вы должны использовать [StructLayout (LayoutKind.Explicit)] и объявить упаковку, чтобы убедиться, что структура struct идентична двоичному формату данных в файле.

Для последних версий C# вы можно использовать Span, как указано Маром c в другом ответе!

0 голосов
/ 20 января 2020

Вы на самом деле вообще не используете MemoryStream. Ваш BinaryReader обращается к файлу напрямую. Чтобы BinaryReader использовал MemoryStream вместо этого:

Заменить

f.Seek(0, SeekOrigin.Begin);
var r = new BinaryReader(f);

...

while (f.Position < f.Length);

на

memStream.Seek(0, SeekOrigin.Begin);
var r = new BinaryReader(memStream);

...

while(r.BaseStream.Position < r.BaseStream.Length)
...