簡體   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