高效读取结构化二进制数据文件

5
我有以下代码片段,用于读取二进制文件并进行验证:
 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#实现这个?


1
从FileStream直接创建BinaryReader,删除MemoryStream。编辑:就像你已经做的那样...那么在你的代码中,MemoryStream的用途是什么?使用这段代码会导致文件被读取两次! - Selvin
4个回答

4

定义一个带有显式布局([StructLayout(LayoutKind.Explicit)])的struct(可能是只读的readonly struct),它与您的C++代码完全相同,然后执行以下操作之一:

  1. 将文件作为内存映射文件打开,获取数据的指针;在原始指针上使用unsafe代码,或在数据上使用Unsafe.AsRef<YourStruct>,并使用Unsafe.Add<>进行迭代。
  2. 将文件作为内存映射文件打开,获取数据的指针;创建一个自定义内存(用于您的T的指针),并在范围内迭代。
  3. 将文件作为一个byte[]打开;在byte[]上创建一个Span<byte>,然后使用MemoryMarshal.Cast<,>创建一个Span<YourType>,并对其进行迭代。
  4. 将文件作为一个byte[]打开;使用fixed来锁定byte*并获取指针;使用unsafe代码遍历指针。
  5. 涉及“管道”的某些内容 - 一个作为缓冲区的Pipe,可能使用FileStream上的StreamConnection填充管道,并具有从管道中出列的工作循环;复杂性:缓冲区可以是不连续的,并且可能在不方便的位置拆分;可以解决,但需要微妙的代码,每当第一个范围小于8字节时。
任何这些概念的组合都应该像您的C++版本一样工作。第4个方法很简单,但对于非常大的数据,您可能更喜欢使用内存映射文件。

2

感谢大家提供的非常有帮助的评论和答案。在获得这些反馈后,这是我首选的解决方案:

      [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

这是我们使用的内容(与旧版本的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)]并声明打包,以确保结构布局与文件中数据的二进制格式相同。
对于最近版本的C#,您可以像Marc在其他答案中提到的那样使用Span!

1
这个方案可以运行,但与仅使用MemoryMappedFile和一些unsafe技巧(甚至在使用Span<>的乐趣和优雅之前)相比,它似乎是“艰难的方式”。此外,虽然与此无关,但在最近的C#中:where T : unmanagedsizeof(T) - 或者在旧版C#中:Unsafe.Sizeof<T>()(除非您实际上正在使用Marshal复制规则,而不是原始类型操作)。 - Marc Gravell
1
@MarcGravell 我想避免使用 unsafe 应该是可取的,否则它会传播到调用不安全方法的所有内容!出于这个原因,我们整个代码库中没有使用 unsafe - Matthew Watson
是的,我无法反驳这一点;这就是为什么我如此喜欢 span :) - Marc Gravell
@MarcGravell确实,如果您能使用它,Span是非常好的!(顺便说一下,我刚刚检查了那段代码的历史记录,似乎是我在2007年编写的... ;) - Matthew Watson

0

目前你实际上并没有使用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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接