在C#中快速将350M个数字加载到double[]数组中

15

我打算将350M个预先计算好的双精度数字存储在一个二进制文件中,并在我的dll启动时将它们加载到内存中。有没有内置的方法可以并行加载它,或者我应该自己将数据分成多个文件,并自己处理多个线程?

回答评论:我将在足够强大的盒子上运行此dll,很可能只在64位机器上运行。因为所有对我的数字的访问都将通过属性进行,所以我可以将数字存储在几个数组中。

[更新]

大家好,谢谢回答!我期待在不同的盒子上进行很多基准测试。 关于需求:我想加快一个非常慢的计算速度,所以我要预先计算一个网格,将其加载到内存中,然后插值。


我不明白为什么你需要将文件分成多个部分。你知道有多少行(假设每行只有一个数字),因此你可以让不同的线程从不同的偏移量开始读取文件。 - FrustratedWithFormsDesigner
4
这不就像2.6吉字节的数据吗? - Anthony Pegram
5
为什么大家都认为 OP 会在一台性能较弱的电脑上运行它? - liori
2
你考虑过使用内存映射文件吗?.Net 4.0允许您轻松地将文件加载到内存中,并且一旦实现了这一点,您就可以使用MemoryMappedViewAccessor直接访问每个数字。大多数实现使用Windows本机代码,因此我认为性能会很好。 - CriGoT
我已经收藏了这个问题。如果可能的话,我想听到原帖作者使用的解决方案以及其效果如何。 - Steven Evers
8个回答

13
我进行了一项小测试,我强烈建议使用内存映射文件。我创建了一个包含350M个双精度值的文件(正如之前许多人提到的那样,大小为2.6GB),然后测试了将文件映射到内存所需的时间,以及访问任何元素所需的时间。
在我的笔记本电脑上进行的所有测试(Win7、.Net 4.0、Core2 Duo 2.0 GHz、4GB RAM)中,映射文件所需的时间都不到一秒钟,在那一点上访问任何元素几乎需要0毫秒(所有时间都用于验证索引)。 然后,我决定遍历所有350M个数字,整个过程大约需要3分钟(包括分页),因此如果您必须迭代,可能还有另一种选择。
尽管如此,出于示例目的,我对访问进行了包装,您在使用此代码之前应检查许多条件。代码如下:
以下是一个关于IT技术的类的代码,这个类可以用来操作内存映射文件。这个类实现了IDisposable接口和IEnumerable<T>接口,其中T是一个结构体类型。这个类有一个构造函数,需要传入一个文件路径作为参数。如果文件路径为空或者文件不存在,会抛出相应的异常。这个类还有一个Length属性,可以获取元素数量。这个类还有一个索引器,可以通过索引获取元素的值。这个类还实现了GetEnumerator()方法,可以返回一个IEnumerator<T>类型的迭代器,可以用foreach语句进行遍历。这个类还有一个静态方法GetArray(string filePath),可以返回一个T类型的数组,可以用来读取内存映射文件中的所有元素。在使用这个类的时候,可以参考下面的示例代码。

Stopwatch watch = Stopwatch.StartNew();
using (Storage<double> helper = new Storage<double>("Storage.bin"))
{
    Console.WriteLine("初始化时间: {0}", watch.ElapsedMilliseconds);
string item; long index;
Console.Write("要显示的项: "); while (!string.IsNullOrWhiteSpace((item = Console.ReadLine()))) { if (long.TryParse(item, out index) && index >= 0 && index < helper.Length) { watch.Reset(); watch.Start(); double value = helper[index]; Console.WriteLine("访问时间: {0}", watch.ElapsedMilliseconds); Console.WriteLine("项: {0}", value); } else { Console.Write("无效的索引"); }
Console.Write("要显示的项: "); } }

更新:我添加了一个静态方法,将文件中的所有数据加载到数组中。显然,这种方法最初需要更多的时间(在我的笔记本电脑上需要1到2分钟),但之后的访问性能符合你对 .Net 的期望。如果您需要频繁访问数据,则此方法应该很有用。

使用方法非常简单:

double[] helper = Storage<double>.GetArray("Storage.bin");

希望这可以帮到你。


我很感谢您的建议,谢谢!但是我没有选择它,因为仅从压缩文件中读取已经足够快,并且更简单。 - A-K
非常好的知识,肯定最好“保持简单”。 - CriGoT

9

看起来很难将这个内容存储在连续的内存数组中,因此并行加载的方式取决于实际的数据结构。

(补充说明:LukeH 在评论中指出,CLR 中实际上存在一个 2GB 的硬性对象大小限制。详见这个其他 SO 问题。)

假设你要从一个磁盘中读取整个文件,将磁盘读取并行化可能不是一个好主意。如果您需要对数字进行处理或在加载后进行处理,您可能需要考虑同时并行运行这些操作。


1
我可以编写一个程序,在我的五年旧笔记本电脑上在内存中保持那么多的连续数组数据。这并不是那么不可能。 - liori
2
@lion:只是好奇……你有多少内存?在什么时候会出现“OutOfMemoryException”?我的甚至无法创建350M条目的双精度数组。 - Brian Gideon
1
我的8GB内存的Dell Precision工作站无法完成此任务。 - mqp
5
@mquander:RAM并不重要,重要的是连续的虚拟地址空间。 RAM只是硬件性能优化;它与“内存耗尽”完全无关。内存是“地址空间”,而不是“RAM”。 - Eric Lippert
2
liori:仅为了那个数组就需要2.8GB的RAM - 你认真的吗,你那台5年前的笔记本比这还要多RAM,并且还是64位?那在当时一定是个非常好的笔记本了... - Peter
显示剩余7条评论

7
您可能已经回答了第一个问题:“这必须要预先计算吗?”是否有一些算法可以使计算所需的值在需要时进行计算以避免此问题?假设没有...
这只有2.6GB的数据 - 在64位处理器上,像这样的少量数据不会有任何问题。但如果您正在运行5年前的计算机和10年前的操作系统,则无法启动,因为这么多数据将立即填满32位应用程序的可用工作集。
在C ++中显然的方法之一是使用内存映射文件。这使得数据对于您的应用程序看起来像是在RAM中,但实际上操作系统仅在访问时分页其中的位,因此使用的真实RAM很少。我不确定您是否可以直接从C#中执行此操作,但您可以轻松地在C ++ / CLI中执行此操作,然后从C#中访问它。
或者,假设已回答“您是否需要同时将其全部加载到RAM中”的问题为“是”,则无法选择任何虚拟化方法,因此...
在多个线程中加载不会有帮助 - 您将受到I / O限制,因此您将有n个线程等待数据(并要求硬盘在它们正在读取的块之间进行寻道),而不是一个线程等待数据(这是按顺序读取的,没有寻道)。因此,线程只会导致更多的寻道,从而可能使其变慢。(唯一的情况是如果将数据分成不同的物理磁盘,则可以并行读取不同的数据块 - 不要在软件中执行此操作;购买RAID阵列)
唯一可能有所帮助的地方是使用多线程使负载在后台发生,同时允许用户在其余缓冲区填充时开始使用已加载的部分数据,因此用户(希望)不必等待数据加载。
因此,您需要在单个线程中将数据加载到一个大数组中...
但是,通过压缩数据,您可能能够显着加快此过程。有几种通用方法值得考虑:
  • 如果您了解数据的一些信息,您可以发明一种编码方案,使数据更小(因此加载速度更快)。例如,如果值相互接近(例如想象描述正弦波的数据点-值从非常小到非常大范围,但每个值仅比上一个值略微增加),则可以使用浮点数表示“增量”,而不会损失原始双精度值的精度,将数据大小减半。如果数据具有对称性或重复性,则可以利用它(例如,想象存储描述整个圆的所有位置,与存储一个象限并使用一些简单且快速的数学方法反射它4次-这是将数据I/O减少四分之一的一种简单方法)。数据大小的任何减少都将导致相应的加载时间缩短。此外,许多这些方案都允许数据在RAM中保持“编码”,因此您将使用远少于RAM,但仍能够在需要时快速获取数据。

  • 或者,您可以非常容易地使用通用压缩算法(例如Deflate)包装流。这可能行不通,但通常通过CPU解压缩数据的成本要低于通过加载较少源数据节省的I/O时间,因此结果是它加载速度显着更快。当然,还可以节省大量磁盘空间。


5
在典型情况下,加载速度将受到正在加载数据的存储速度的限制,即硬盘。
如果要加快速度,您需要使用更快的存储器,例如多个硬盘组成的RAID方案。
如果您的数据可以合理压缩,请这样做。尝试找到一个算法,它将正好使用和您拥有的CPU功率一样多的CPU功率——不足此值,则外部存储速度将是限制因素; 超过此值,则CPU速度将是限制因素。如果您的压缩算法可以使用多个内核,则多线程可能会有用。
如果您的数据在某种程度上是可预测的,您可能希望提出自定义压缩方案。例如,如果连续数字彼此相近,则可能希望存储数字之间的差异,这可能有助于提高压缩效率。
您真的需要双精度吗?也许浮点数就可以胜任工作了?也许您不需要完整的双倍范围?例如,如果您需要完整的53位尾数精度,但只需要存储介于-1.0和1.0之间的数字,则可以通过不在完整范围内存储指数来裁剪每个数字的几个位。

3
将此并行化是一个不好的想法,除非你正在使用SSD。限制因素将是磁盘IO,如果你运行两个线程,磁头将在被读取的两个区域之间来回跳动。这将比任何可能的并行加速慢得多。
请记住,驱动器是机械设备,与处理器相比非常缓慢。如果您可以执行一百万条指令以避免单个磁头寻道,您仍然会获得优势。
另外,一旦文件在磁盘上,请确保对磁盘进行碎片整理,以确保它在一个连续的块中。

2

我认为这并不是一个好主意。350,000,000 * 8字节 = 2,800,000,000字节。即使你设法避免OutOfMemoryException,该进程仍然可能在页面文件中进行换入/换出操作。你可以把数据留在文件中,根据需要逐步加载较小的块。重要的是,仅仅因为你可以分配那么多内存,并不意味着你应该这样做。


3
你在这里对OP将使用的机器作出了很多假设。 - Martin Smith
2
@Martin:OP有责任说明硬件限制是什么;如果没有任何信息,那么保守推理是有意义的。坦白地说,我也觉得这不是一个好主意。如果我面对着3.5亿个浮点数在磁盘上,我永远不会尝试一次性将它们全部读入内存。我会根据需要分块读取它们。这是一个非常明智的想法。 - Eric Lippert
1
@Martin:这是一个很好的观点,特别是考虑到问题的编辑。我已经相应地编辑了我的答案。 - Brian Gideon
1
@Eric Lippert:如果OP问到一个有1000个元素的数组,你会要求他声明他不写8051的程序吗? - liori
2
@lion:我可以绝对肯定地说,任何能运行.NET应用程序的计算机都同样能够分配一个8KB的数组。 - Brian Gideon
@Brian Gideon:他没有说他会运行.NET。也许是Mono,也许是Portable.NET... - liori

1

通过适当的磁盘配置,将文件分割成多个跨磁盘文件可能是有意义的 - 并且在单独的线程中读取每个文件会很好地工作(如果您有一些条带化 - RAID什么的 :) - 那么从一个具有多个线程的单个文件中读取可能是有意义的)。

我认为,如果只使用单个物理磁盘,尝试这样做可能会徒劳无功。


0
刚看到这个:.NET 4.0 支持内存映射文件。这将是一种非常快速的方法,不需要支持并行化等。

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