繁体   English   中英

使用数组字段而不是大量对象

[英]Using array fields instead of massive number of objects

根据这篇文章,我想知道人们使用 arrays 来存储数据字段而不是实例化数百万个对象并增加 memory 开销(例如,12每个 object 24 字节,具体取决于您阅读的文章)。 每个属性的数据因项目而异,因此我不能使用严格的享元模式,但会设想类似的东西。

我对这种表示的想法是,一个人有一个“模板对象”......

class Thing
{
  double A;
  double B;
  int    C;
  string D;
}

然后是一个容器 object 以及根据要求创建 object 的方法...

class ContainerOfThings
{
  double[] ContainerA;
  double[] ContainerB;
  int[]    ContainerC;
  string[] ContainerD;

  ContainerOfThings(int total)
  {
    //create arrays
  }

  IThing GetThingAtPosition(int position)
  {
     IThing thing = new Thing(); //probably best done as a factory instead
     thing.A = ContainerA[position];
     thing.B = ContainerB[position];
     thing.C = ContainerC[position];
     thing.D = ContainerD[position];

     return thing;
  }
}

所以这是一个简单的策略,但不是很通用,例如,如果不复制数据并破坏数组字段存储的目的,就无法创建“事物”的子集(作为列表)。 我还没有找到好的例子,所以我会很感激链接或代码片段的更好的方法来处理这个场景的人已经完成了......或者一个更好的主意。

这取决于您的具体情况。 根据创建对象的频率,您可以:

  1. 如果对象是可序列化的,则将它们保存在 MemoryMappedFile 中(获得中/低性能和低 memory 消耗的一些融合)。

  2. Map 不同对象之间的字段:我的意思是,如果 object 最初具有默认值,则将它们全部放在单独的基础上,如果该值与默认值不同,则真正分配一个新空间。 (这自然对引用类型有意义)。

  3. 另一个解决方案再次将对象保存到 SqlLite 基础。 比 MemoryMappedFiles 更容易管理,因为您可以使用简单的 SQL。

选择取决于您,因为这取决于您的具体项目要求。

问候。

根据这篇文章,我想知道人们在使用 arrays 来存储数据字段而不是实例化数百万个对象并增加 memory 开销的内存中存储大量数据集(例如,>10,000,000 个对象)的经验是什么……

我想有几种方法可以解决这个问题,实际上您正在寻找一种可能的解决方案来限制 memory 中的数据。 但是,我不确定是否将您的结构减少了 24 个? 字节会给你带来很多好处。 您的结构大约是 79 个字节(对于 15 个字符的字符串)= 8 + 8 + 4 + 24? + 4 + 1 + (2 * 字符长度) 所以你的总收益最多为 25%。 这似乎不是很有用,因为您必须在 position 中,其中 1000 万 * 80 字节适合 memory 而 1000 万 * 100 字节不适合。 这意味着您设计的解决方案处于灾难边缘,过多的大字符串或过多的记录,或其他一些占用 memory 的程序,并且您的机器已超出 memory。

如果您需要支持对 n 个小记录的随机访问,其中 n = 1000 万,那么您的目标应该是设计至少 2n 或 10n。 也许您已经在您的 1000 万中考虑了这一点? 无论哪种方式,都有很多技术可以支持这种类型的数据被访问。

一种可能性是,如果字符串的最大长度 (ml) 限制为合理大小(例如 255),那么您可以 go 到一个简单的 ISAM 存储。 每条记录将是 8 + 8 + 4 + 255 字节,您可以简单地偏移到一个平面文件中来读取它们。 如果记录大小是可变的或可能很大,那么您将需要为此使用不同的存储格式并将偏移量存储到文件中。

另一种可能性是,如果您通过某个键查找值,那么我会推荐嵌入式数据库或 BTree 之类的东西,您可以禁用某些磁盘一致性来获得性能。 碰巧我为大量数据的客户端缓存编写了一个 BPlusTree。 使用 B+Tree 的详细信息在这里

实际上 ADO.NET DataTable 使用类似的方法来存储数据。 也许你应该看看它是如何在那里实现的。 因此,您需要有一个类似于 DataRow 的 object,它在内部保存指向表的指针和行数据的索引。 这将是我相信的最轻量级的解决方案。

在您的情况下:a)如果您每次调用 GetThingAtPosition 方法时都在构建事物,那么您会在堆中创建 object,这会使表中已有的信息加倍。 加上“对象开销”数据。

b) 如果您需要访问 ContainerOfThings 中的每个项目,则所需的 memory 将加倍 + 12 字节 * 对象开销数。 在这种情况下,最好有一个简单的数组而不是即时创建它们。

你的问题暗示有问题。 memory 的使用是否被证明是一个问题?

如果每个项目 100 字节,那么它听起来像 1GB。 所以我想知道这个应用程序是否有问题。 该应用程序是在具有 8GB 或 ram 的专用 64 位机器上运行吗?

如果有恐惧,您可以通过集成测试来测试恐惧。 实例化 2000 万个这样的项目并运行一些性能测试。

但当然,这一切都来自应用程序域。 我有专门的应用程序使用比这更多的内存并且运行良好。 硬件成本通常远低于软件成本(是的,它再次归结为应用程序域)。

再见

不幸的是,OO 不能抽象出性能问题(带宽饱和就是其中之一)。 这是一个方便的范例,但它有局限性。

我喜欢你的想法,我也使用这个......你猜怎么着,我们不是第一个想到这个的;-)。 我发现它确实需要一些思想转变。

我可以推荐你去 J 社区吗? 看:

http://www.JSoftware.com

那不是 C#(或 Java)组。 他们是一群好人。 通常需要将阵列视为第一个 class object。 在 C# 中,它几乎没有那么灵活。 与 C# 一起工作可能是一个令人沮丧的结构。

大型数据集问题有各种 OO 模式......但如果你问这样的问题,可能是时候让 go 更实用一些了。 或者至少可以解决问题/原型设计。

我为 rapidSTORM 项目做了这样的事情,其中需要缓存数百万个人口稀少的对象(定位显微镜)。 虽然我不能真正为您提供好的代码片段(依赖项太多),但我发现使用 Boost Fusion 实现非常快速和直接。 融合结构,为每个元素类型构建一个向量,然后为该向量编写一个非常简单的访问器来重建每个元素。

(哦,我刚刚注意到你标记了这个问题,但也许我的 C++ 答案也有帮助)

[2011-07-19更新]

现在有一个新版本可用: http://www.mediafire.com/file/74fxj7u1n0ppcq9/MemStorageDemo-6639584-2011_07_19-12_47_00.ZADCDBD79A8D84175C229B192AADC02F2

我仍在尝试调试一些令人讨厌的引用计数,但是从新的 xUnit session 开始,我能够运行创建 1000 万个对象的测试(它发生在我身上,我现在已经减小了字符串大小以进行测试,但是我让它运行了 1000 万个可变长度的字符串,长度从 3 到 15 字节,我还没有机会尝试比这更大。在我的系统上,我的 10 负载从大约 ~1.95G 到 ~2.35G百万个对象,除了使用实际托管字符串的非常简单的支持 class 之外,我还没有对字符串做任何事情。

无论如何,所以,我认为它工作得相当好,尽管在后备存储上肯定还有优化工作要做,而且我还认为必要时可以在迭代器上做一些工作,这取决于你一次处理的数据量. 不幸的是,直到明天晚些时候或第二天才能再次查看它。

无论如何,这是基本的想法:

  1. MemoryArray class:使用由Marhsal.AllocHGlobal()分配的非托管 memory 来存储结构。 我正在使用一个大的MemoryArray进行测试,但就成员而言,只有几个字段,我认为只要你保持数组大小相当大,memory 的消耗不会有太大差异。 MemoryArray实现了IEnumerable<MemoryArrayItem> ,这是我在测试中用来填充 arrays 的。

MemoryArray旨在保存来自任何被支持的 object 的常规大小的数据块。 您可以使用我尚未实现的指针数学对枚举器执行一些操作。 我目前每次都返回新对象,所以这是我相信的很大一部分。 在原型 class 中,我基于这个原型,我能够使用非常常规的指针数学来进行遍历,但是我认为我这样做的方式主要用于非常快速的遍历,并且可能不适用于互操作性。

MemoryArray只有一个标准索引器,它使用Int Ptr 上的指针数学来获取请求的元素,该指针表示在构造时分配的非托管数据的头部。 我还实现了一个名义上的 2D 索引器,您可以在其中将表示维度的整数数组传递给数组,然后可以对其执行 A[x,y]。 这只是它如何工作的一个简单的小例子。

我还没有实现的一件事是任何形式的分段,但我确实认为一个分段适合该项目,所以当我有机会时我可能会实现它。

  1. MemoryArrayEnumerator class:我选择实现实际的枚举器而不是枚举器函数。 枚举器 class 基本上只需要一个MemoryArray ,然后为枚举器提供库存函数以返回实际的MemoryArrayItem对象。

  2. MemoryArrayItem class:这个 class 除了保存基于数组中的 start 和 position 的适当指针信息外,并没有做太多事情。 它是在这个 object 之上(旁边?)实现的特定类,它们实际上是通过指针来获取数据。

然后还有一些支持类, MemoryStringArray是可变大小的 memory 支持块,暂时只处理字符串,然后是自动处理 class ( AutoDisposer ) 和通用 ZA2F2ED4F8EBC2CBB4C21 AutoReference<> )。

现在,在这个基础之上是特定的类。 这三种类型(数组/枚举器/项)中的每一种都是专门为您正在查看的对象实现的。 在我放弃的这个项目的更大规模版本中,这是一个峰值,我对偏移量有更通用的处理,这样你就不会那么依赖于具体的类,但即使它们是很有用; 我最初的实现都是没有真正基础的具体类。

目前我将它们全部实现为您将引用传递给的单独类; 因此, TestArray class 在其构造函数中传递了MemoryArray class。 与枚举数和项目相同。 我知道那里会节省一些处理能力,我认为如果我能找到一种将它们作为后代实现的好方法,而不仅仅是拥有底层类的副本,我认为也很有可能节省空间。 不过,我想先获得它的基本感觉,这似乎是 go 最直接的方法。 问题是它是另一层间接性。

事实证明, TestArrayTestArrayEnumerator除了传递MemoryArrayMemoryArrayEnumerator的功能外并没有做太多事情。 这些类中的主要问题只是将指针分割并传递到使用它的项目中。

但是,所以 TestArrayItem 是指针实际转换为真实数据的地方; 这是文件。 我剪掉了一大段评论,这些评论通过一些选项来更好地处理可变长度后备存储(仍然在上面给出的链接中的实际文件中),请原谅我给自己留下注释的大量评论我在做这件事时在想什么:)

TestArrayItem.cs

// ----------------------------------------------
//      rights lavished upon all with love
//         see 'license/unlicense.txt'
//   ♥ 2011, shelley butterfly - public domain
// ----------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MemStorageDemo
{
    public unsafe class TestArrayItem
    {
        public static MemoryStringArray s_MemoryArrayStore = new MemoryStringArray();

        public static int TestArrayItemSize_bytes =
            sizeof(double) * 2
            + sizeof(int)
            + sizeof(int);

        // hard-coding here; this is another place that things could be a little more generic if you wanted and if 
        // performance permitted; for instance, creating a dictionary of offsets based on index.  also, perhaps
        // a dictionary with key strings to allow indexing using field names of the object.
        private enum EFieldOffset
        {
            DoubleTheFirstOffset       =  0,
            DoubleTheSecondOffset      =  8,
            IntTheFirstOffset          = 16,
            StringTheFirstHandleOffset = 20
        }

        private MemoryArrayItem myMemoryArrayItem;
        private MemoryStringArray myStringStore;

        // constructor that uses the static string array store
        public TestArrayItem(MemoryArrayItem parMemoryArrayItem) :
            this(parMemoryArrayItem, s_MemoryArrayStore)
        {
        }

        // constructor for getting the item at its memory block without any initialization (e.g. existing item)
        public TestArrayItem(MemoryArrayItem parMemoryArrayItem, MemoryStringArray parStringStore)
        {
            myMemoryArrayItem = parMemoryArrayItem;
            myStringStore = parStringStore;
        }

        // constructor for geting the item at its memory block and initializing it (e.g. adding new items)
        public TestArrayItem(MemoryArrayItem parMemoryArrayItem, double parDoubleTheFirst, double parDoubleTheSecond, int parIntTheFirst, string parStringTheFirst)
        {
            myMemoryArrayItem = parMemoryArrayItem;

            DoubleTheFirst = parDoubleTheFirst;
            DoubleTheSecond = parDoubleTheSecond;
            IntTheFirst = parIntTheFirst;
            StringTheFirst = parStringTheFirst;
        }

        // if you end up in a situation where the compiler isn't giving you equivalent performance to just doing
        // the array math directly in the properties, you could always just do the math directly in the properties.
        //
        // it reads much cleaner the way i have it set up, and there's a lot less code duplication, so without 
        // actually determining empirically that i needed to do so, i would stick with the function calls.
        private IntPtr GetPointerAtOffset(EFieldOffset parFieldOffset)
            { return myMemoryArrayItem.ObjectPointer + (int)parFieldOffset; }

        private double* DoubleTheFirstPtr 
            { get { return (double*)GetPointerAtOffset(EFieldOffset.DoubleTheFirstOffset); } }
        public double DoubleTheFirst
        {
            get
            {
                return *DoubleTheFirstPtr;
            }

            set
            {
                *DoubleTheFirstPtr = value;
            }
        }

        private double* DoubleTheSecondPtr
            { get { return (double*)GetPointerAtOffset(EFieldOffset.DoubleTheSecondOffset); } }
        public double DoubleTheSecond
        {
            get
            {
                return *DoubleTheSecondPtr;
            }
            set
            {
                *DoubleTheSecondPtr = value;
            }
        }

        // ahh wishing for a preprocessor about now
        private int* IntTheFirstPtr
            { get { return (int*)GetPointerAtOffset(EFieldOffset.IntTheFirstOffset); } }
        public int IntTheFirst
        {
            get
            {
                return *IntTheFirstPtr;
            }
            set
            {
                *IntTheFirstPtr = value;
            }
        }

        // okay since we're using the StringArray backing store in the example, we just need to get the
        // pointer stored in our blocks, and then copy the data from that address 
        private int* StringTheFirstHandlePtr 
            { get { return (int*)GetPointerAtOffset(EFieldOffset.StringTheFirstHandleOffset); } }
        public string StringTheFirst
        {
            get
            {
                return myStringStore.GetString(*StringTheFirstHandlePtr);
            }
            set
            {
                myStringStore.ModifyString(*StringTheFirstHandlePtr, value);
            }
        }

        public void CreateStringTheFirst(string WithValue)
        {
            *StringTheFirstHandlePtr = myStringStore.AddString(WithValue);
        }

        public override string ToString()
        {
            return string.Format("{0:X8}: {{ {1:0.000}, {2:0.000}, {3}, {4} }} {5:X8}", (int)DoubleTheFirstPtr, DoubleTheFirst, DoubleTheSecond, IntTheFirst, StringTheFirst, (int)myMemoryArrayItem.ObjectPointer);
        }
    }
}

所以,这才是真正的魔力; 只是基本上实现了根据有关字段的信息找出正确指针的函数。 就目前而言,我认为它是代码生成的一个很好的候选者,好吧,假设我让附加/分离的东西正常工作。 我隐藏了很多必须使用自动类型指针进行手动 memory 管理的事情,我认为从长远来看这将是值得调试的......

无论如何,就是这样,我希望今晚或明天回来上网,我会尽力办理登机手续。不幸的是,go 将电缆调制解调器还给电缆公司,所以不会营业除非我们 go 去麦当劳什么的:)希望这对您有所帮助; 我将用它调试问题,以便至少有一个功能基础可供使用; 我知道这不是我第一次考虑编写这样的库,我想其他人也有。


以前的内容

我使用类似这样的东西来使用我创建的 COM 库,以与预编译的 win32/64 FFTW dll 互操作。 对于这个问题,我们确实需要比我所拥有的更通用的东西,所以我上周开始研究一些足以作为这些类型用途的体面通用库的东西,具有可扩展的 memory 管理,多维,切片,等等

好吧,我昨天终于对自己承认(a)它还需要几天才能准备好,(b)我需要一个尖峰解决方案来找出最后的一些问题。 所以,我决定在较低的抽象层次上进行第一次切割,试图满足您问题中解决的需求。 不幸的是,我们即将搬家,我必须做一些打包,但我认为这可能足以解决您的问题。

这周我将继续研究这个例子,我会在这里更新新代码,我会尝试从中提取足够的信息来发表一篇解释它的帖子,但如果你仍然感兴趣,这是最后一个我'可能会在本周晚些时候发布:

http://www.mediafire.com/file/a7yq53ls18q7bvf/EfficientStorage-6639584.zip

它只是一个 VS2010 解决方案,具有做我认为满足您需求的基本必需品。 有很多评论随处可见,但请随时提出问题,一旦我有互联网,我会回来查看......无论哪种方式,这都是一个有趣的项目。

完成示例后,我打算完成第一次迭代完整库并将其发布到某个地方; 当我有它时,我会在这里更新链接。

强制警告:它可以编译但未经测试; 我敢肯定有问题,这是一个相当大的话题。

您为您的类型中的每个属性创建一个 System.Array 数组。 这些子数组的大小等于您拥有的对象数。 财产访问将是:

masterArray[propertyIndex][objectIndex]

这将允许您使用值类型 arrays 而不是 object 的 arrays。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM