【发布时间】: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 与处理交错来提高整体处理速度。