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

A-K*_*A-K 14 c# parallel-processing

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

回答评论:我将在足够强大的盒子上运行这个dll,很可能只在64位的盒子上运行.因为无论如何所有对我的号码的访问都是通过属性,我可以将我的号码存储在几个数组中.

[更新]

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

Cri*_*GoT 12

好吧,我做了一个小测试,我肯定会建议使用内存映射文件.我创建了一个包含350M双值的文件(前面提到的2.6 GB)然后测试了将文件映射到内存然后访问任何元素所花费的时间.

在我的笔记本电脑(Win7,.Net 4.0,Core2 Duo 2.0 GHz,4GB RAM)中进行的所有测试中,映射文件所需的时间不到一秒,此时访问任何元素的时间几乎为0毫秒(所有时间都在验证索引).然后我决定通过所有350M的数字,整个过程花了大约3分钟(包括分页)所以如果在你的情况下你必须迭代他们可能是另一种选择.

尽管如此,我还是将访问权限包装起来,仅作为示例,在使用此代码之前应该检查很多条件,它看起来像这样


public class Storage<T> : IDisposable, IEnumerable<T> where T : struct
{
    MemoryMappedFile mappedFile;
    MemoryMappedViewAccessor accesor;
    long elementSize;
    long numberOfElements;

    public Storage(string filePath)
    {
        if (string.IsNullOrWhiteSpace(filePath))
        {
            throw new ArgumentNullException();
        }

        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException();
        }

        FileInfo info = new FileInfo(filePath);
        mappedFile = MemoryMappedFile.CreateFromFile(filePath);
        accesor = mappedFile.CreateViewAccessor(0, info.Length);
        elementSize = Marshal.SizeOf(typeof(T));
        numberOfElements = info.Length / elementSize;
    }

    public long Length
    {
        get
        {
            return numberOfElements;
        }
    }

    public T this[long index]
    {
        get
        {
            if (index < 0 || index > numberOfElements)
            {
                throw new ArgumentOutOfRangeException();
            }

            T value = default(T);
            accesor.Read<T>(index * elementSize, out value);
            return value;
        }
    }

    public void Dispose()
    {
        if (accesor != null)
        {
            accesor.Dispose();
            accesor = null;
        }

        if (mappedFile != null)
        {
            mappedFile.Dispose();
            mappedFile = null;
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        T value;
        for (int index = 0; index < numberOfElements; index++)
        {
            value = default(T);
            accesor.Read<T>(index * elementSize, out value);
            yield return value;
        }
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        T value;
        for (int index = 0; index < numberOfElements; index++)
        {
            value = default(T);
            accesor.Read<T>(index * elementSize, out value);
            yield return value;
        }
    }

    public static T[] GetArray(string filePath)
    {
        T[] elements;
        int elementSize;
        long numberOfElements;

        if (string.IsNullOrWhiteSpace(filePath))
        {
            throw new ArgumentNullException();
        }

        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException();
        }

        FileInfo info = new FileInfo(filePath);
        using (MemoryMappedFile mappedFile = MemoryMappedFile.CreateFromFile(filePath))
        {
            using(MemoryMappedViewAccessor accesor = mappedFile.CreateViewAccessor(0, info.Length))
            {
                elementSize = Marshal.SizeOf(typeof(T));
                numberOfElements = info.Length / elementSize;
                elements = new T[numberOfElements];

                if (numberOfElements > int.MaxValue)
                {
                    //you will need to split the array
                }
                else
                {
                    accesor.ReadArray<T>(0, elements, 0, (int)numberOfElements);
                }
            }
        }

        return elements;
    }
}

以下是如何使用该类的示例


Stopwatch watch = Stopwatch.StartNew();
using (Storage<double> helper = new Storage<double>("Storage.bin"))
{
    Console.WriteLine("Initialization Time: {0}", watch.ElapsedMilliseconds);

    string item;
    long index;

    Console.Write("Item to show: ");
    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("Access Time: {0}", watch.ElapsedMilliseconds);
            Console.WriteLine("Item: {0}", value);
        }
        else
        {
            Console.Write("Invalid index");
        }

        Console.Write("Item to show: ");
    }
}

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

用法非常简单

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

HTH


mqp*_*mqp 9

听起来你实际上不太可能将它放入内存中的连续数组中,所以可能你并行加载的方式取决于实际的数据结构.

(附录:LukeH在评论中指出CLR中的对象大小实际上有2GB的限制.这在其他SO问题中有详细说明.)

假设您正在从一个磁盘读取整个内容,并行化磁盘读取可能是一个坏主意.如果在加载数字时需要对数字进行任何处理,您可能需要考虑在从磁盘读取的同时并行运行.

  • @mquander:RAM无关紧要.重要的是连续的虚拟地址空间.RAM只是硬件性能优化; 它与"耗尽内存"没有任何关系.内存是*地址空间*,而不是*RAM*. (5认同)
  • @lion:好奇......你有多少内存,你在什么时候得到`OutOfMemoryException`?我甚至无法接近创建350M入口双阵列. (2认同)
  • liori:它只需要2.8GB的RAM用于那个阵列 - 你认真的是你5岁的笔记本有更多的RAM而且是64位吗?当时一定是笔记本电脑的问题...... (2认同)

Jas*_*ams 7

您可能已经回答的第一个问题是"这是否必须预先计算?".是否有一些算法可以根据需要计算所需的值来避免这个问题?假设没有......

这只有2.6GB的数据 - 在64位处理器上,你可以轻松获得这样的少量数据.但是,如果您使用的是具有10年运行版操作系统的5岁计算机,那么它就是非启动程序,因为很多数据将立即填充32位应用程序的可用工作集.

在C++中显而易见的一种方法是使用内存映射文件.这使得数据在您的应用程序中看起来就好像它在RAM中一样,但操作系统实际上只在访问它时才会对其进行分页,因此使用的实际RAM非常少.我不确定你是否可以直接从C#中执行此操作,但是你可以很容易地在C++/CLI中执行此操作,然后从C#访问它.

或者,假设"你是否需要同时在RAM中同时使用所有这些"的问题已经回答"是",那么你就不能采用任何一种虚拟化方法,所以...

加载多个线程无济于事 - 你将受到I/O限制,因此你将有n个线程等待数据(并要求硬盘驱动器在他们正在读取的块之间寻找)而不是一个线程waiitng for数据(按顺序读取,没有搜索).所以线程只会导致更多的搜索,因此可能会使它变得更慢.(将数据拆分的唯一情况可能有帮助,如果将其拆分为不同的物理磁盘,以便可以并行读取不同的数据块 - 不要在软件中执行此操作;购买RAID阵列)

多线程可能有用的唯一地方是在应用程序的其余部分启动时使负载在后台运行,并允许用户在缓冲区的其余部分填充时开始使用已加载的数据部分,因此用户(希望)在加载数据时不必等待太多.

所以,你又回到了将数据加载到单个线程中的一个大型数组中......

但是,您可以通过压缩数据来大大提高速度.有几种一般方法需要考虑:

  • 如果您对数据有所了解,您可能会发明一种编码方案,使数据更小(因此加载速度更快).例如,如果值往往彼此接近(例如,想象描述正弦波的数据点 - 值范围从非常小到非常大,但每个值只是从最后一个小的增量)您可能能够表示浮点数中的"增量"而不会丢失原始双精度值的精度,将数据大小减半.如果数据存在任何对称性或重复性,您可以利用它(例如,想象存储所有位置来描述整个圆,而不是存储一个象限并使用一些简单快速的数学来反映它4次 - 简单的方法来计算I/O数据量.数据大小的任何减少都会相应地减少加载时间.此外,许多这些方案都允许数据在RAM中保持"编码",因此您可以使用更少的RAM,但仍然能够在需要时快速获取数据.

  • 或者,您可以使用通用压缩算法(如Deflate)轻松地包装流.这可能不起作用,但通常解压缩CPU上的数据的成本小于通过加载较少的源数据节省的I/O时间,因此最终结果是它加载速度明显加快.当然,也节省了大量的磁盘空间.


lio*_*ori 5

在典型情况下,加载速度将受到加载数据的存储速度的限制 - 即硬盘驱动器.

如果您希望它更快,您需要使用更快的存储,多个硬盘驱动器加入RAID方案.

如果您的数据可以合理压缩,请执行此操作.尝试找到算法,它将使用与你一样多的CPU功率 - 小于此值,你的外部存储速度将是限制因素; 超过这个,你的CPU速度将是限制因素.如果您的压缩算法可以使用多个内核,那么多线程可能很有用.

如果您的数据在某种程度上是可预测的,您可能想要提出自定义压缩方案.Fe如果连续的数字彼此接近,您可能希望存储数字之间的差异 - 这可能有助于提高压缩效率.

你真的需要双精度吗?也许花车会做这个工作?也许你不需要全方位的双打?例如,如果你需要完整的53位尾数精度,但只需要存储介于-1.0和1.0之间的数字,你可以尝试通过不在全范围存储指数来切断每个数字的几位.