在 C# 中处理大内存列表时如何避免垃圾排序问题
How to avoid garbage collation problems when dealing with large in memory lists in c#
我们的文章-标签关系 SQL 性能存在一些问题,因此我们决定将 article/tags 保留在内存中,这给了我们显着的提升,但它现在导致我们当整个列表被删除并替换为新列表(3m + 记录)时,垃圾收集令人头疼。
这是一段代码:
private readonly IContextCreator _contextCreator;
private volatile static List<TagEngineCacheResponse> _cachedList = new List<TagEngineCacheResponse>();
private readonly int KEYWORD_GROUP_NAME = 1;
private static BitmapIndex.BitmapIndex _bitMapIndex = new BitmapIndex.BitmapIndex();
public TagEngineService(IContextCreator contextCreator)
{
_contextCreator = contextCreator;
}
public async Task RepopulateEntireCacheAsync()
{
using (var ctx = _contextCreator.PortalContext())
{
var cmd = ctx.Database.Connection.CreateCommand();
cmd.CommandText = BASE_SQL_QUERY;
await ctx.Database.Connection.OpenAsync();
var reader = await cmd.ExecuteReaderAsync();
var articles = ((IObjectContextAdapter)ctx)
.ObjectContext
.Translate<TagEngineCacheResponse>(reader).ToList();
//recreate bitmap indexes
BitmapIndex.BitmapIndex tempBitmapIndex = new BitmapIndex.BitmapIndex();
int recordRow = 0;
foreach (var record in articles)
{
tempBitmapIndex.Set(new BIKey(KEYWORD_GROUP_NAME, record.KeywordId), recordRow);
recordRow++;
}
_cachedList = articles;
_bitMapIndex = tempBitmapIndex;
}
}
Class定义:
public class TagEngineCacheResponse
{
public int ArticleId { get; set; }
public int KeywordId { get; set; }
public DateTime PublishDate { get; set; }
public int ViewCountSum { get; set; }
}
如您所见,当缓存被重新创建时,_cachedList 被替换为一个新的列表,而旧的则准备好被垃圾收集。此时,cpu GC 时间跳到 60-90%,持续 2-3 秒。
有什么想法可以改进这段代码以避免 GC 问题吗?
我猜这个列表每个对象大约需要 44 个字节,或者 3m 个对象大约需要 130Mb。这有点偏大,但并非难以置信。
一些建议:
该列表远远超过小对象堆 (SOH) 的 87k 限制,因此它将分配在大对象堆 (LOH) 上。这仅在第 2 代收集,第 2 代收集可能很昂贵。为避免这种情况,建议尽可能避免取消分配 gen2 对象,即分配一次,然后尽可能重用它们。
您可以以较小的块从数据库中获取列表并就地更新列表。确保每个块都在 SOH 的限制内。您可以考虑锁定列表以确保在更新时不访问它,或者在更新一个列表的地方保留两个交替列表,然后切换 'active' 列表。
您正在为 TagEngineCacheResponse
使用 class,这将导致分配大量对象。虽然它们足够小以适合 SOH,但如果您不走运,它们可能会存活足够长的时间以放入第 2 代堆。虽然 GC 时间不会受到未引用对象的很大影响,但使用值类型并避免该问题可能仍然更好。配置文件以确保它确实有帮助。
我们的文章-标签关系 SQL 性能存在一些问题,因此我们决定将 article/tags 保留在内存中,这给了我们显着的提升,但它现在导致我们当整个列表被删除并替换为新列表(3m + 记录)时,垃圾收集令人头疼。
这是一段代码:
private readonly IContextCreator _contextCreator;
private volatile static List<TagEngineCacheResponse> _cachedList = new List<TagEngineCacheResponse>();
private readonly int KEYWORD_GROUP_NAME = 1;
private static BitmapIndex.BitmapIndex _bitMapIndex = new BitmapIndex.BitmapIndex();
public TagEngineService(IContextCreator contextCreator)
{
_contextCreator = contextCreator;
}
public async Task RepopulateEntireCacheAsync()
{
using (var ctx = _contextCreator.PortalContext())
{
var cmd = ctx.Database.Connection.CreateCommand();
cmd.CommandText = BASE_SQL_QUERY;
await ctx.Database.Connection.OpenAsync();
var reader = await cmd.ExecuteReaderAsync();
var articles = ((IObjectContextAdapter)ctx)
.ObjectContext
.Translate<TagEngineCacheResponse>(reader).ToList();
//recreate bitmap indexes
BitmapIndex.BitmapIndex tempBitmapIndex = new BitmapIndex.BitmapIndex();
int recordRow = 0;
foreach (var record in articles)
{
tempBitmapIndex.Set(new BIKey(KEYWORD_GROUP_NAME, record.KeywordId), recordRow);
recordRow++;
}
_cachedList = articles;
_bitMapIndex = tempBitmapIndex;
}
}
Class定义:
public class TagEngineCacheResponse
{
public int ArticleId { get; set; }
public int KeywordId { get; set; }
public DateTime PublishDate { get; set; }
public int ViewCountSum { get; set; }
}
如您所见,当缓存被重新创建时,_cachedList 被替换为一个新的列表,而旧的则准备好被垃圾收集。此时,cpu GC 时间跳到 60-90%,持续 2-3 秒。
有什么想法可以改进这段代码以避免 GC 问题吗?
我猜这个列表每个对象大约需要 44 个字节,或者 3m 个对象大约需要 130Mb。这有点偏大,但并非难以置信。
一些建议:
该列表远远超过小对象堆 (SOH) 的 87k 限制,因此它将分配在大对象堆 (LOH) 上。这仅在第 2 代收集,第 2 代收集可能很昂贵。为避免这种情况,建议尽可能避免取消分配 gen2 对象,即分配一次,然后尽可能重用它们。
您可以以较小的块从数据库中获取列表并就地更新列表。确保每个块都在 SOH 的限制内。您可以考虑锁定列表以确保在更新时不访问它,或者在更新一个列表的地方保留两个交替列表,然后切换 'active' 列表。
您正在为 TagEngineCacheResponse
使用 class,这将导致分配大量对象。虽然它们足够小以适合 SOH,但如果您不走运,它们可能会存活足够长的时间以放入第 2 代堆。虽然 GC 时间不会受到未引用对象的很大影响,但使用值类型并避免该问题可能仍然更好。配置文件以确保它确实有帮助。