首页 > 解决方案 > 从数据库中检索值时内存使用率高

问题描述

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

我的问题是:

  1. 这是预期的行为吗?
  2. 当我只想检索一个文档时,如何避免如此巨大的内存分配?

这是探查器的完全可复制的示例和屏幕截图:

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; }
}

更改185_0001_850_000使我的 RAM 使用量达到 >4GB(!)

探查器: 探查器图像

标签: c#litedb

解决方案


在 LiteDB 中分配比直接更多的内存有几个原因List<Double>

要理解这一点,您需要知道您的类型化类已转换为BsonDocument结构(带有BsonValues)。此结构有开销(每个 +1 或 +5 个字节BsonValue)。

此外,为了序列化这个类(当你插入时),LiteDB 必须创建一个byte[]包含所有这些的单个BsonDocument(以 BSON 格式)。之后,这个超大byte[]文件被复制到许多扩展页面(每个页面包含一个byte[4070])。

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

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

对于小物体,这个操作是可以的。该内存被重复用于每个新文档的读/写,因此内存保持可控。

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

  • 反序列化不需要将所有数据分配到单个byte[]读取文档中。这可以使用 new 来完成ChunkStream(IEnumerable<byte[]>)。序列化还是需要这个单byte[]
  • 日志文件已更改为 WAL(预写日志) - 不需要保留原始数据。
  • ExtendPage不再存储在缓存中

对于未来的版本,我正在考虑使用新Span<T>类来重新使用以前的内存分配。但我需要对此进行更多研究。


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

尝试将您的班级分成 2 个集合:一个用于您的数据,另一个用于每个值。您还可以将这个大数组拆分成块,例如 LiteDB FileStorage 或 MongoDB GridFS。


推荐阅读