从数据库中检索值时内存使用率高
High memory usage when retrieving values from database
我有一个项目,我必须在其中存储 16 个对象,每个对象包含 185 000 个 double
的列表。已保存对象的总大小应该在 20-30 MB 左右 (sizeof(double) * 16 * 185 000
),但是当我尝试从数据库中检索它时,数据库分配了 200 MB 来检索这个 20-30 MB 的对象。
我的问题是:
- 这是预期的行为吗?
- 当我只想
检索一份文件?
这里是完全可复制的示例和分析器的屏幕截图:
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(!)
探查器:
首先,您创建列表的方式,它将为 262.144 个元素预留空间,因为它是 growth algorithm。
你应该事先设置项目的数量以避免这种情况(或者可能只使用一个数组):
Values = new List<double>(max);
Values.AddRange(Enumerable.Range(0, max).Select(v => (double)v));
就 LiteDB 而言,如果您不需要数据库(以及它带来的潜在开销),只需将其存储在您自己的数据结构中即可。如果您实际上不使用数据库并且只存储单个项目,我看不到数据库有任何好处。
在 LiteDB 中分配比直接分配更多的内存有几个原因 List<Double>
。
要理解这一点,您需要知道您键入的 class 已转换为 BsonDocument
结构(具有 BsonValues
)。此结构有开销(每个 BsonValue
+1 或 +5 字节)。
此外,要序列化此 class(当您插入时),LiteDB 必须创建一个包含所有这些 BsonDocument
(BSON 格式)的单个 byte[]
。之后,这个超大byte[]
被复制到很多扩展页面(每个页面包含一个byte[4070]
)。
不仅如此,LiteDB 还必须跟踪原始数据以存储在日志区域中。所以,这个尺寸可以翻倍。
要反序列化,LiteDB 必须执行逆过程:将所有页面从磁盘读取到内存,将所有页面合并为一个 byte[]
,反序列化为 BsonDocument
以完成映射到您的 class.
这个操作,对于小对象,是可以的。每个新文档都会重复使用此内存 read/write,因此内存始终处于控制之中。
在下一个 v5 版本中,这个过程有一些优化,例如:
- 反序列化不需要将所有数据分配到单个
byte[]
中来读取文档。这可以使用 new ChunkStream(IEnumerable<byte[]>)
来完成。连载还需要这个单byte[]
- 日志文件已更改为 WAL(预写日志)- 不需要保留原始数据。
ExtendPage
不再存储在缓存中
对于未来的版本,我考虑使用 new Span<T>
class 来重新使用以前的内存分配。但是我需要更多地研究这个。
但是,存储具有 185,000 个值的单个文档是任何 nosql 数据库中的最佳解决方案。 MongoDB 将 BSON 文档大小限制为 16Mb(早期版本限制为 ~368kb)...我在 v2 中将 LiteDB 限制为 1Mb...但我删除了此检查大小并仅作为建议添加以避免大型单个文档。
尝试将您的 class 分成 2 个集合:一个用于您的数据,另一个用于每个值。您还可以将这个大数组拆分成多个块,例如 LiteDB FileStorage 或 MongoDB GridFS。
我有一个项目,我必须在其中存储 16 个对象,每个对象包含 185 000 个 double
的列表。已保存对象的总大小应该在 20-30 MB 左右 (sizeof(double) * 16 * 185 000
),但是当我尝试从数据库中检索它时,数据库分配了 200 MB 来检索这个 20-30 MB 的对象。
我的问题是:
- 这是预期的行为吗?
- 当我只想 检索一份文件?
这里是完全可复制的示例和分析器的屏幕截图:
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(!)
探查器:
首先,您创建列表的方式,它将为 262.144 个元素预留空间,因为它是 growth algorithm。
你应该事先设置项目的数量以避免这种情况(或者可能只使用一个数组):
Values = new List<double>(max);
Values.AddRange(Enumerable.Range(0, max).Select(v => (double)v));
就 LiteDB 而言,如果您不需要数据库(以及它带来的潜在开销),只需将其存储在您自己的数据结构中即可。如果您实际上不使用数据库并且只存储单个项目,我看不到数据库有任何好处。
在 LiteDB 中分配比直接分配更多的内存有几个原因 List<Double>
。
要理解这一点,您需要知道您键入的 class 已转换为 BsonDocument
结构(具有 BsonValues
)。此结构有开销(每个 BsonValue
+1 或 +5 字节)。
此外,要序列化此 class(当您插入时),LiteDB 必须创建一个包含所有这些 BsonDocument
(BSON 格式)的单个 byte[]
。之后,这个超大byte[]
被复制到很多扩展页面(每个页面包含一个byte[4070]
)。
不仅如此,LiteDB 还必须跟踪原始数据以存储在日志区域中。所以,这个尺寸可以翻倍。
要反序列化,LiteDB 必须执行逆过程:将所有页面从磁盘读取到内存,将所有页面合并为一个 byte[]
,反序列化为 BsonDocument
以完成映射到您的 class.
这个操作,对于小对象,是可以的。每个新文档都会重复使用此内存 read/write,因此内存始终处于控制之中。
在下一个 v5 版本中,这个过程有一些优化,例如:
- 反序列化不需要将所有数据分配到单个
byte[]
中来读取文档。这可以使用 newChunkStream(IEnumerable<byte[]>)
来完成。连载还需要这个单byte[]
- 日志文件已更改为 WAL(预写日志)- 不需要保留原始数据。
ExtendPage
不再存储在缓存中
对于未来的版本,我考虑使用 new Span<T>
class 来重新使用以前的内存分配。但是我需要更多地研究这个。
但是,存储具有 185,000 个值的单个文档是任何 nosql 数据库中的最佳解决方案。 MongoDB 将 BSON 文档大小限制为 16Mb(早期版本限制为 ~368kb)...我在 v2 中将 LiteDB 限制为 1Mb...但我删除了此检查大小并仅作为建议添加以避免大型单个文档。
尝试将您的 class 分成 2 个集合:一个用于您的数据,另一个用于每个值。您还可以将这个大数组拆分成多个块,例如 LiteDB FileStorage 或 MongoDB GridFS。