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

时间:2018-08-21 12:39:57

标签: c# litedb

我有一个项目,其中我必须存储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_000更改为1_850_000会使我的RAM使用量达到> 4GB(!)

分析器: Profiler image

2 个答案:

答案 0 :(得分:1)

LiteDB中分配比直接List<Double>多得多的内存的原因很多。

要了解这一点,您需要知道将您键入的类转换为BsonDocument结构(使用BsonValues)。这种结构会有开销(每个BsonValue +1或+5字节)。

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

不仅如此,LiteDB还必须跟踪原始数据以存储在日记帐区域中。因此,此大小可以加倍。

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

对于小物体,此操作是可以的。每次读取/写入新文档时都会重复使用此内存,因此内存将始终处于控制状态。

在下一v5版本中,此过程进行了一些优化,例如:

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

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


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

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

答案 1 :(得分:0)

首先,创建列表的方式将为growth algorithm保留262.144个元素的空间。

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

Values = new List<double>(max);
Values.AddRange(Enumerable.Range(0, max).Select(v => (double)v));

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