C# 从数据库检索值时内存使用率高

C# 从数据库检索值时内存使用率高,c#,litedb,C#,Litedb,我有一个项目,其中我必须存储16个对象,每个对象包含185000个double。保存的对象的总大小应该在20-30 mb左右(sizeof(double)*16*185000),但当我尝试从数据库检索它时,数据库会分配200 mb来检索这个20-30 mb的对象 我的问题是: 这是预期的行为吗 当我只想这样做时,我如何避免如此巨大的内存分配 检索一个文档 以下是profiler的完全可复制示例和屏幕截图: class Program { private static string _pa

我有一个项目,其中我必须存储16个对象,每个对象包含185000个
double
。保存的对象的总大小应该在20-30 mb左右(
sizeof(double)*16*185000
),但当我尝试从数据库检索它时,数据库会分配200 mb来检索这个20-30 mb的对象

我的问题是:

  • 这是预期的行为吗
  • 当我只想这样做时,我如何避免如此巨大的内存分配 检索一个文档 以下是profiler的完全可复制示例和屏幕截图:

    class Program
    {
        private static string _path;
    
        static void Main(string[] args)
        {
            _path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "testDb");
    
            // Comment after first insert to avoid adding the same object.
            AddData();
    
            var data = GetData();
    
            Console.ReadLine();
        }
    
        public static void AddData()
        {
            var items = new List<Item>();
            for (var index = 0; index < 16; index++)
            {
                var item = new Item {Values = Enumerable.Range(0, 185_000).Select(v => (double) v).ToList()};
                items.Add(item);
            }
            var testData = new TestClass { Name = "Test1", Items = items.ToList() };
    
            using (var db = new LiteDatabase(_path))
            {
                var collection = db.GetCollection<TestClass>();
                collection.Insert(testData);
            }
        }
    
        public static TestClass GetData()
        {
            using (var db = new LiteDatabase(_path))
            {
                var collection = db.GetCollection<TestClass>();
                // This line causes huge memory allocation and wakes up garbage collector many many times.
                return collection.FindOne(Query.EQ(nameof(TestClass.Name), "Test1"));
            }
        }
    }
    
    public class TestClass
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IList<Item> Items { get; set; }
    }
    
    public class Item
    {
        public IList<double> Values { get; set; }
    }
    
    类程序
    {
    私有静态字符串_路径;
    静态void Main(字符串[]参数)
    {
    _path=path.Combine(AppDomain.CurrentDomain.BaseDirectory,“testDb”);
    //在第一次插入后添加注释,以避免添加相同的对象。
    AddData();
    var data=GetData();
    Console.ReadLine();
    }
    公共静态void AddData()
    {
    var items=新列表();
    对于(var指数=0;指数<16;指数++)
    {
    var item=newitem{Values=Enumerable.Range(0185_000)。选择(v=>(double)v.ToList();
    项目。添加(项目);
    }
    var testData=newtestclass{Name=“Test1”,Items=Items.ToList()};
    使用(var db=new LiteDatabase(_path))
    {
    var collection=db.GetCollection();
    collection.Insert(testData);
    }
    }
    公共静态测试类GetData()
    {
    使用(var db=new LiteDatabase(_path))
    {
    var collection=db.GetCollection();
    //这一行会导致巨大的内存分配,并多次唤醒垃圾收集器。
    return collection.FindOne(Query.EQ(nameof(TestClass.Name),“Test1”);
    }
    }
    }
    公共类TestClass
    {
    公共int Id{get;set;}
    公共字符串名称{get;set;}
    公共IList项{get;set;}
    }
    公共类项目
    {
    公共IList值{get;set;}
    }
    
    185_000
    更改为
    1_850_000
    会使我的RAM使用率变为>4GB(!)

    探查器:

    首先,按照您创建列表的方式,它将为262.144个元素预留空间,因为它的空间有限

    您应该事先设置项目数量以避免出现这种情况(或者可能只是一起使用数组):

    值=新列表(最大值);
    Values.AddRange(Enumerable.Range(0,max).Select(v=>(double)v));
    

    就LiteDB而言,如果您不需要数据库(以及它带来的潜在开销),只需将其存储在您自己的数据结构中即可。如果您不实际使用数据库而只存储单个项目,我看不到数据库的任何好处。

    在LiteDB中分配比direct
    List
    多得多的内存有几个原因

    要理解这一点,您需要知道键入的类被转换为
    BsonDocument
    结构(使用
    BsonValues
    )。此结构具有开销(+1或+5字节/BsonValue)

    此外,要序列化此类(插入时),LiteDB必须使用所有这些
    BsonDocument
    (BSON格式)创建一个
    字节[]
    。之后,这个超大的
    字节[]
    被复制到许多扩展页面(每个页面包含一个
    字节[4070]

    不仅如此,LiteDB还必须跟踪原始数据以存储在日志区域中。所以,这个尺寸可以加倍

    要反序列化,LiteDB必须执行相反的过程:将所有页面从磁盘读取到内存,将所有页面连接到单个
    字节[]
    ,反序列化到
    BsonDocument
    ,以完成到类的映射

    对于小对象,此操作正常。每次读/写新文档时都会重新使用该内存,因此内存可以控制

    在下一个v5版本中,此过程有一些优化,如:

    • 反序列化不需要将所有数据分配到单个
      字节[]
      中即可读取文档。这可以使用新的
      ChunkStream(IEnumerable)
      完成。序列化仍然需要这个
      字节[]
    • 日志文件已更改为WAL(提前写入日志)-不需要保留原始数据
    • ExtendPage
      不再存储在缓存中
    对于未来的版本,我想使用新的
    Span
    类来重用以前的内存分配。但我需要更多的研究


    但是,存储一个具有185000个值的文档是任何nosql数据库中的最佳解决方案。MongoDB将BSON文档大小限制为16Mb(早期版本限制为~368kb)。。。我将v2中的LiteDB限制为1Mb。。。但我删除了这个检查大小,只是作为建议添加,以避免大型单个文档


    尝试将类拆分为两个集合:一个用于数据,另一个用于每个值。您还可以将这个大数组拆分为块,如LiteDB FileStorage或MongoDB GridFS。

    @Caramiriel您的意思是什么?您是指
    可枚举的.Range(0185_000)
    还是探查器屏幕截图?现在我看到了BsonArray,这使得它更可能成为LiteDB框架中的问题。
    这是预期的行为吗?
    我不是
    LiteDB
    专家,但这不会让我感到惊讶。一个大型数组将位于大型对象堆上。如果它是作为读取过程的一部分克隆的,那么这可以解释一些RAM的使用。如果它被转换成或从另一种格式(如BSON)转换而来,这可能也解释了它的一部分。从根本上说,你关心的是什么。NET是一个垃圾收集的运行时,它使用的RAM比您想象的要多,这并没有什么害处。你为什么担心?@mjwills我知道复制会增加内存使用,但从20MB增加到>200MB是荒谬的。如果我将其更改为创建1185000个元素,那么由于ram的使用,它将变得不稳定。我不是nosql专家,但为200 mb的文件分配>4gb似乎不太合适。如果是这样,我怀疑您需要尝试其他数据库平台,看看
    Values = new List<double>(max);
    Values.AddRange(Enumerable.Range(0, max).Select(v => (double)v));