在C#中直接读取大型二进制文件而不进行复制

7

我正在寻找最高效/直接的方法来执行这个简单的C/C++操作:

void ReadData(FILE *f, uint16 *buf, int startsamp, int nsamps)
{
   fseek(f, startsamp*sizeof(uint16), SEEK_SET);
   fread(buf, sizeof(uint16), nsamps, f);
}

在C#/.NET中,我需要读取存储在二进制文件中的许多(可能是10到100百万)2字节(16位)“ushort”整数数据样本(固定格式,无需解析)。 C方式的好处在于它直接将样本读入“uint16 *”缓冲区中,没有CPU参与,也没有复制。是的,这可能是“不安全”的,因为它使用未知大小的缓冲区的void *指针,但似乎应该有一个“安全”的.NET替代品。
在C#中,最佳方法是什么?我找了一些提示(使用FieldOffset的“联合”,使用指针的“不安全”代码,马歇尔),但似乎都不适用于这种情况,而且需要进行某种形式的复制/转换。我想避免BinaryReader.ReadUInt16(),因为它非常缓慢和CPU密集。在我的计算机上,使用ReadUInt16()的for()循环和使用单个Read()直接读取字节到byte []数组之间的速度差约为25倍。在非阻塞I/O(重叠“有用”处理,同时等待磁盘I/O)时,这个比率可能会更高。
理想情况下,我想要简单地将ushort []数组伪装成byte []数组,以便可以直接用Read()填充它,或者以某种方式使Read()直接填充ushort []数组:
// DOES NOT WORK!!
public void GetData(FileStream f, ushort [] buf, int startsamp, int nsamps)
{
    f.Position = startsamp*sizeof(ushort);
    f.Read(buf, 0, nsamps);
}

但是,没有一个接受ushort[]数组的Read()方法,只有一个接受byte[]数组的方法。

在C#中能直接完成这个任务吗?或者我需要使用非托管代码或第三方库,或者必须采用耗费CPU的逐个样本转换?虽然“安全”更可取,但我可以使用“不安全”的代码或某些使用Marshal的技巧,我只是还没有弄明白。

感谢任何指导!


[更新]

我想添加一些代码,如dtb所建议的那样,因为似乎很少有ReadArray的示例。这是一个非常简单的示例,没有显示错误检查。

public void ReadMap(string fname, short [] data, int startsamp, int nsamps)
{
    var mmf = MemoryMappedFile.CreateFromFile(fname);
    var mmacc = mmf.CreateViewAccessor();

    mmacc.ReadArray(startsamp*sizeof(short), data, 0, nsamps);
}

数据安全地转储到您传递的数组中。您还可以为更复杂的类型指定类型。它似乎能够自行推断简单类型,但使用类型说明符,它会像这样:

    mmacc.ReadArray<short>(startsamp*sizeof(short), data, 0, nsamps);

[更新2]

我想按照Ben的获胜答案所建议的方式添加代码,以“裸骨”形式类似于上面的代码进行比较。这段代码已经编译和测试过,可以正常工作,并且速度很快。我直接在DllImport中使用了SafeFileHandle类型(而不是更常见的IntPtr),以简化事情。

[DllImport("kernel32.dll", SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
static extern bool ReadFile(SafeFileHandle handle, IntPtr buffer, uint numBytesToRead, out uint numBytesRead, IntPtr overlapped);

[DllImport("kernel32.dll", SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
static extern bool SetFilePointerEx(SafeFileHandle hFile, long liDistanceToMove, out long lpNewFilePointer, uint dwMoveMethod);

unsafe void ReadPINV(FileStream f, short[] buffer, int startsamp, int nsamps)
{
    long unused; uint BytesRead;
    SafeFileHandle nativeHandle = f.SafeFileHandle; // clears Position property
    SetFilePointerEx(nativeHandle, startsamp*sizeof(short), out unused, 0);

    fixed(short* pFirst = &buffer[0])
        ReadFile(nativeHandle, (IntPtr)pFirst, (uint)nsamps*sizeof(short), out BytesRead, IntPtr.Zero);
}

如果您不想使用 BinaryReader.ReadUInt16();,那么您可能需要将数据读入字节数组,然后再处理该字节数组。即使您把它分成块,100M 2字节的数据也约为200MB,因此您应该能够一次性将其全部读入内存并处理。 - Nate
2
fread 可能不是零拷贝 I/O,它是有缓冲的(所有 stdio.h 函数都可以有缓冲,并且在大多数实现中都是如此)。 - Ben Voigt
Ben,虽然操作系统可能在幕后进行复制,但我试图避免程序本身的额外复制。 - dale
严格来说,C运行时库并不是操作系统的一部分。 - Ben Voigt
我运行了一些测试,目前最快的方法是使用File.ReadAllBytes读取内存中的所有字节,然后使用BitConverter将它们转换为ushort数组。内存映射文件需要大约60%的时间。另一方面,使用异步流操作或TPL可以通过交错IO和处理来加快整体处理速度。 - Panagiotis Kanavos
3个回答

8
你可以使用MemoryMappedFile。在内存映射文件后,你可以创建一个视图(即MemoryMappedViewAccessor),该视图提供了一个ReadArray<T>方法。此方法可以从文件中读取结构体而无需进行编组,并且它与原始类型(如ushort)一起工作。

2
如果你使用的是.NET 4,这是一个很好的方法。它比Dale想要模仿的C代码甚至更少地进行复制。在早期版本的.NET中,您可能需要p/invoke ReadFile来模仿C代码,或者p/invoke CreateFileMapping以获得更快的方式。 - Ben Voigt
dtb,谢谢,我之前没有看到ReadArray(),甚至Google也不是很了解它!它看起来非常方便。我做了一些计时,它的速度大约是使用ReadUInt16()的for()循环的两倍,所以我怀疑它在幕后进行了一些复制(读取字节而不进行转换仍然快约10倍)。我看到Accessor类有许多类似于BinaryReader的方法。我想知道微软是否最终会向BinaryReader添加一个ReadArray()方法,这样我们就可以直接从流中读取结构,而无需经过内存映射。 - dale
当然,你是对的。由于.NET元数据存储在与内容相同的内存块中,它别无选择,只能进行复制。如果你使用P/Invoke调用CreateFileReadFile,并传递一个指向ushort[]的第一个元素的指针(需要使用不安全代码),你应该能够以与读取byte[]相同的速度读取数据。 - Ben Voigt
嘿,你不需要使用p/invoke CreateFile,你可以直接将FileStreamSafeFileHandle属性传递给SetFilePositionReadFile - Ben Voigt
好的,我在我的回答中加入了更详细的解释和示例代码。 - Ben Voigt
显示剩余3条评论

3
dtb的回答 更加优秀(实际上,它也必须复制数据,没有优势),但我只是想指出,如果要从一个字节数组中提取ushort值,应该使用BitConverter而不是BinaryReader 编辑:调用ReadFile的示例代码:
[DllImport("kernel32.dll", SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
static extern bool ReadFile(IntPtr handle, IntPtr buffer, uint numBytesToRead, out uint numBytesRead, IntPtr overlapped);

[DllImport("kernel32.dll", SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
static extern bool SetFilePointerEx(IntPtr hFile, long liDistanceToMove, out long lpNewFilePointer, uint dwMoveMethod);

unsafe bool read(FileStream fs, ushort[] buffer, int offset, int count)
{
  if (null == fs) throw new ArgumentNullException();
  if (null == buffer) throw new ArgumentNullException();
  if (offset < 0 || count < 0 || offset + count > buffer.Length) throw new ArgumentException();
  uint bytesToRead = 2 * count;
  if (bytesToRead < count) throw new ArgumentException(); // detect integer overflow
  long offset = fs.Position;
  SafeFileHandle nativeHandle = fs.SafeFileHandle; // clears Position property
  try {
    long unused;
    if (!SetFilePositionEx(nativeHandle, offset, out unused, 0);
    fixed (ushort* pFirst = &buffer[offset])
      if (!ReadFile(nativeHandle, new IntPtr(pFirst), bytesToRead, out bytesToRead, IntPtr.Zero)
        return false;
    if (bytesToRead < 2 * count)
      return false;
    offset += bytesToRead;
    return true;
  }
  finally {
    fs.Position = offset; // restore Position property
  }
}

1
Ben,谢谢,我看了一下BitConverter(),但我不确定我理解你的建议。BinaryReader()是用于从文件中读取(我正在这样做),而BitConverter()是用于将现有的byte[]数组转换为其他类型。BinaryReader().ReadUInt16()不等同于将字节读入数组并调用BitConverter().ToUInt16()吗?也许我误解了... - dale
不是这样的。BinaryReader 负责将基础流的字节转换为请求的类型,而不是首先从 IO 中读取字节。 - Panagiotis Kanavos
1
顺便提一下:BitConverter的数组版本(不需要不安全代码或指针,但仍然需要复制):http://msdn.microsoft.com/en-us/library/system.buffer.blockcopy.aspx - Ben Voigt
@Andrew:肯定缺少使用声明,并且需要放在类内部。你还有其他问题吗?但请使用问题末尾的代码,该代码遵循此方法但添加了改进。 - Ben Voigt
@BenVoigt:我通过将“external”方法中的IntPtr更改为SafeFileHandle来使其正常工作。但是,我也没有注意到巨大的性能提升。我发现它与您推荐的Buffer.BlockCopy类似。 - poy
显示剩余5条评论

2
我可能有点晚了,但我发现最快的方法是使用之前答案的组合。
如果我按照以下步骤操作:
MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(somePath);
Stream io = mmf.CreateViewStream();

int count;
byte[] byteBuffer = new byte[1024 << 2];
ushort[] dataBuffer = new ushort[buffer.Length >> 1];

while((count = io.Read(byteBuffer, 0, byteBuffer.Length)) > 0)
  Buffer.BlockCopy(buffer, 0, dataBuffer, 0, count);

这比被接受的答案快了约2倍。

对我来说,unsafe方法与没有MemoryMappedFileBuffer.BlockCopy相同。 MemoryMappedFile缩短了一点时间。


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