【问题标题】:Directly reading large binary file in C# w/out copying在 C# 中直接读取大型二进制文件,无需复制
【发布时间】:2011-03-13 11:50:02
【问题描述】:

我正在寻找最有效/最直接的方法来执行这个简单的 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);

[UPATE2]

我想添加 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,所以你应该能够一次将其全部读入内存并进行处理。
  • fread 可能不是零拷贝 I/O,它是被缓冲的(所有 stdio.h 函数都可以被缓冲并且在大多数实现中)。
  • Ben,承认操作系统可能会在后台进行复制,但我试图避免的是程序本身的额外复制。
  • 严格来说,C 运行时库不是操作系统的一部分。
  • 我运行了一些测试,迄今为止最快的方法被证明是使用 File.ReadAllBytes 读取内存中的所有字节,然后使用 BitConverter 将它们转换为 ushort 数组。内存映射文件需要大约 60% 的时间。另一方面,使用异步流操作或 TPL 可以通过将 IO 与处理交错来提高整体处理速度。

标签: c# .net file binary


【解决方案1】:

您可以使用MemoryMappedFile。对文件进行内存映射后,您可以创建一个视图(即MemoryMappedViewAccessor),该视图提供ReadArray<T> 方法。此方法无需编组即可从文件中读取结构,并且适用于原始类型 lie ushort

【讨论】:

  • 如果您有 .NET 4,这是一个很好的方法。它的复制比 dale 想要模拟的 C 代码还要少。在旧版本的 .NET 中,您可能必须 p/invoke ReadFile 来模拟 C 代码,或者 p/invoke CreateFileMapping 以获得这种更快的方式。
  • dtb,谢谢,我还没有看到 ReadArray(),事实上,甚至 Google 也不是很了解它!它看起来像一个非常方便的工具。我做了一些计时,它的速度大约是使用 ReadUInt16() 的 for() 循环的两倍,所以我怀疑它在后台进行了一些复制(读取不带转换的字节仍然快 10 倍左右)。我看到 Accessor 类有很多与 BinaryReader 类似的方法。我想知道 MS 是否最终可以将 ReadArray() 方法添加到 BinaryReader,然后我们可以直接从流中读取结构而无需通过内存映射。
  • 您当然是对的,因为 .NET 元数据与内容存储在同一个内存块中,所以它别无选择,只能复制。如果您 P/Invoke CreateFileReadFile 传递指向 ushort[] 的第一个元素的指针(需要不安全的代码),您应该获得与读取 byte[] 相同的速度。
  • 另外,您本身不需要unsafe 代码块,您可以使用msdn.microsoft.com/en-us/library/…
  • 哦,嘿,您不需要 p/invoke CreateFile,您可以将FileStreamSafeFileHandle 属性直接传递给SetFilePositionReadFile
【解决方案2】:

dtb's answer 是一个更好的方法(实际上,它也必须复制数据,没有任何收获),但我只是想指出从一个字节中提取 ushort 值你应该使用BitConverter而不是BinaryReader的数组

编辑:p/调用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
  }
}

【讨论】:

  • Ben,谢谢,我查看了 BitConverter(),但我不确定我是否理解您的建议。 BinaryReader() 用于从文件中读取(我正在这样做),而 BitConverter() 用于将现有的 byte[] 数组转换为其他类型。 BinaryReader().ReadUInt16() 不等于将字节读入数组并调用 BitConverter().ToUInt16() 吗?可能是我误会了……
  • 但是 ReadUInt16 一次只能读取一个元素...这是一种糟糕的 I/O 方式。
  • 不,不是。 BinaryReader 负责将底层流的字节转换为请求的类型,而不是首先从 IO 读取字节。
  • 啊,谢谢本。我错过了 BitConverter() 可以对数组进行操作。谢谢。
  • 顺便说一句:BitConverter 的数组版本(不需要不安全的代码或指针,但仍然可以复制):msdn.microsoft.com/en-us/library/system.buffer.blockcopy.aspx
【解决方案3】:

我在这里玩游戏可能有点晚了......但我发现最快的方法是使用以前答案的组合。

如果我执行以下操作:

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 节省了一点时间。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-12-19
    • 1970-01-01
    • 2012-08-19
    • 2019-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多