C# OutOfMemory、映射内存文件或临时数据库

Posted

技术标签:

【中文标题】C# OutOfMemory、映射内存文件或临时数据库【英文标题】:C# OutOfMemory, Mapped Memory File or Temp Database 【发布时间】:2012-03-16 01:34:58 【问题描述】:

寻求一些建议,最佳实践等......

技术:C# .NET4.0、Winforms、32 位

我正在就如何在我的 C# Winforms 应用程序中最好地处理大型数据处理寻求一些建议,该应用程序遇到高内存使用(工作集)和偶尔的 OutOfMemory 异常。

问题在于,当打开“购物篮”时,我们会在“内存中”执行大量数据处理。 简单来说,当加载“购物篮”时,我们会执行以下计算;

    对于“购物篮”中的每件商品,检索其历史价格一直追溯到该商品首次出现在库存中的日期(可能是两个月、两年或二十年的数据)。历史价格数据是通过互联网从文本文件中检索的,价格插件支持的任何格式。

    对于每件商品,自首次出现在库存以来的每一天,计算各种指标,从而为购物篮中的每件商品建立历史档案。

结果是我们可能会根据“购物篮”中的商品数量执行数百、数千和/或数百万次计算。如果篮子中的项目太多,我们可能会遇到“OutOfMemory”异常。

一些注意事项

    需要为“购物篮”中的每个项目计算此数据,并保留数据直到“购物篮”关闭。

    即使我们在后台线程中执行第 1 步和第 2 步,速度也很重要,因为“购物篮”中的商品数量会极大地影响整体计算速度。

    当“购物篮”关闭时,内存由 .NET 垃圾收集器回收。我们已经分析了我们的应用程序,并确保在关闭篮子时正确处理和关闭所有引用。

    所有计算完成后,结果数据存储在 IDictionary 中。 "CalculatedData 是一个类对象,其属性是通过上述过程计算的各个指标。

我想到的一些想法;

显然,我主要关心的是减少计算使用的内存量,但是只有在我的情况下才能减少使用的内存量 1) 减少每天计算的指标数量或 2) 减少用于计算的天数。

如果我们希望满足我们的业务需求,这两个选项都不可行。

内存映射文件 一种想法是使用将存储数据字典的内存映射文件。这可能/可行吗?我们如何才能将其落实到位?

使用临时数据库 这个想法是使用一个单独的(不是内存中的)数据库,可以为应用程序的生命周期创建它。随着“购物篮”的打开,我们可以将计算出的数据持久化到数据库中以供重复使用,从而减轻了对同一个“购物篮”重新计算的需求。

还有其他我们应该考虑的替代方案吗?对大数据进行计算并在 RAM 之外执行这些计算的最佳做法是什么?

感谢任何建议....

【问题讨论】:

OutOfMemoryException 真的是 "out of address space" - 你考虑过迁移到 64 位吗? 【参考方案1】:

最简单的解决方案是数据库,也许是 SQLite。内存映射文件不会自动成为字典,您必须自己编写所有内存管理代码,从而与 .net GC 系统本身争夺数据的所有权。

【讨论】:

【参考方案2】:

如果您有兴趣尝试内存映射文件方法,现在可以尝试。我编写了一个名为MemMapCache 的小型原生.NET 包,它实质上创建了一个由MemMappedFiles 支持的key/val 数据库。这是一个有点老套的概念,但 MemMapCache.exe 程序保留了对内存映射文件的所有引用,因此如果您的应用程序崩溃,您不必担心丢失缓存状态。

它使用起来非常简单,您应该可以将其放入您的代码中而无需进行太多修改。这是一个使用它的例子:https://github.com/jprichardson/MemMapCache/blob/master/TestMemMapCache/MemMapCacheTest.cs

也许它至少对您有一些用处,以至少进一步弄清楚您需要为实际解决方案做些什么。

如果您最终使用它,请告诉我。我会对你的结果感兴趣。

但是,从长远来看,我会推荐 Redis。

【讨论】:

谢谢JP。 Redis 是一个 NoSQL 数据库,为什么你更喜欢它?你认为它的优点和缺点是什么?不过感谢您的提示,我不知道并会调查。 Redis 非常快。它的命令集非常简单,一个人可以在 30 分钟内学会它。它是一个网络数据库,可以水平扩展。它不限于 1 MB 的对象。 你能回答我的问题吗?***.com/questions/9760073/…【参考方案3】:

对于那些偶然发现这个线程的人的更新......

我们最终使用 SQLite 作为我们的缓存解决方案。我们使用的 SQLite 数据库与应用程序使用的主数据存储区分开存在。我们根据需要将计算的数据保存到 SQLite (diskCache),并使用代码控制缓存失效等。这对我们来说是一个合适的解决方案,因为我们能够实现写入速度和每秒大约 100,000 条记录。

对于那些感兴趣的人,这是控制插入到磁盘缓存中的代码。 这段代码的全部功劳归于 JP Richardson(显示在此处回答问题),因为他的博文非常出色。

internal class SQLiteBulkInsert

#region Class Declarations

private SQLiteCommand m_cmd;
private SQLiteTransaction m_trans;
private readonly SQLiteConnection m_dbCon;

private readonly Dictionary<string, SQLiteParameter> m_parameters = new Dictionary<string, SQLiteParameter>();

private uint m_counter;

private readonly string m_beginInsertText;

#endregion

#region Constructor

public SQLiteBulkInsert(SQLiteConnection dbConnection, string tableName)

    m_dbCon = dbConnection;
    m_tableName = tableName;

    var query = new StringBuilder(255);
    query.Append("INSERT INTO ["); query.Append(tableName); query.Append("] (");
    m_beginInsertText = query.ToString();


#endregion

#region Allow Bulk Insert

private bool m_allowBulkInsert = true;
public bool AllowBulkInsert  get  return m_allowBulkInsert;  set  m_allowBulkInsert = value;  

#endregion

#region CommandText

public string CommandText

    get
    
        if(m_parameters.Count < 1) throw new SQLiteException("You must add at least one parameter.");

        var sb = new StringBuilder(255);
        sb.Append(m_beginInsertText);

        foreach(var param in m_parameters.Keys)
        
            sb.Append('[');
            sb.Append(param);
            sb.Append(']');
            sb.Append(", ");
        
        sb.Remove(sb.Length - 2, 2);

        sb.Append(") VALUES (");

        foreach(var param in m_parameters.Keys)
        
            sb.Append(m_paramDelim);
            sb.Append(param);
            sb.Append(", ");
        
        sb.Remove(sb.Length - 2, 2);

        sb.Append(")");

        return sb.ToString();
    


#endregion

#region Commit Max

private uint m_commitMax = 25000;
public uint CommitMax  get  return m_commitMax;  set  m_commitMax = value;  

#endregion

#region Table Name

private readonly string m_tableName;
public string TableName  get  return m_tableName;  

#endregion

#region Parameter Delimiter

private const string m_paramDelim = ":";
public string ParamDelimiter  get  return m_paramDelim;  

#endregion

#region AddParameter

public void AddParameter(string name, DbType dbType)

    var param = new SQLiteParameter(m_paramDelim + name, dbType);
    m_parameters.Add(name, param);


#endregion

#region Flush

public void Flush()

    try
    
        if (m_trans != null) m_trans.Commit();
    
    catch (Exception ex)
    
        throw new Exception("Could not commit transaction. See InnerException for more details", ex);
    
    finally
    
        if (m_trans != null) m_trans.Dispose();

        m_trans = null;
        m_counter = 0;
    


#endregion

#region Insert

public void Insert(object[] paramValues)

    if (paramValues.Length != m_parameters.Count) 
        throw new Exception("The values array count must be equal to the count of the number of parameters.");

    m_counter++;

    if (m_counter == 1)
    
        if (m_allowBulkInsert) m_trans = m_dbCon.BeginTransaction();
        m_cmd = m_dbCon.CreateCommand();

        foreach (var par in m_parameters.Values)
            m_cmd.Parameters.Add(par);

        m_cmd.CommandText = CommandText;
    

    var i = 0;
    foreach (var par in m_parameters.Values)
    
        par.Value = paramValues[i];
        i++;
    

    m_cmd.ExecuteNonQuery();

    if(m_counter != m_commitMax)
    
        // Do nothing
    
    else
    
        try
        
            if(m_trans != null) m_trans.Commit();
        
        catch(Exception)
         
        finally
        
            if(m_trans != null)
            
                m_trans.Dispose();
                m_trans = null;
            

            m_counter = 0;
        
    


#endregion

【讨论】:

以上是关于C# OutOfMemory、映射内存文件或临时数据库的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中使用内存映射文件时是不是可以避免数据副本?

c# 出现OutofMemory错误

c# 通过内存映射实现文件共享内存

C#大文件读取和查询--内存映射

内存映射文件 VS 命名管道 - C#

使用 Protobuf 和内存映射文件 C# 的 IPC