【问题标题】:Reducing the memory footprint of a C# application减少 C# 应用程序的内存占用
【发布时间】:2010-01-02 00:56:53
【问题描述】:

我正在开发一个需要处理大约 4,000,000 个英文句子的 C# 应用程序。所有这些句子都存储在一棵树中。树中的每个节点都是一个具有这些字段的类:

class TreeNode
{
    protected string word;
    protected Dictionary<string, TreeNode> children;
}

我的问题是应用程序在到达第 2,000,000 句时耗尽了所有 RAM(我有 2 GB RAM)。所以它只处理了一半的句子,然后它就大大减慢了。

我可以做些什么来尝试减少应用程序的内存占用?

编辑:让我再解释一下我的应用程序。所以我有大约 300,000 个英文句子,并且从每个句子中我生成更多的子句子,如下所示:

示例: 一句话:足球是一项非常受欢迎的运动 我需要的子句:

  1. 足球是一项非常受欢迎的运动
  2. 是一项非常受欢迎的运动
  3. 一项非常受欢迎的运动
  4. 非常流行的运动
  5. 流行运动
  6. 运动

每个句子逐字存储在树中。所以考虑上面的例子,我有一个 TreeNode 类,其中包含单词 field = "Football",而子列表中包含单词 "is" 的 TreeNode。 “is”节点的子节点是“a”节点。 “a”节点的子节点是“very”节点。我需要逐字存储句子,因为我需要能够搜索以示例开头的所有句子:“足球是”。

所以基本上对于句子中的每个单词,我都会创建一个新的(子句)。这就是我最终得到 4,000,000 个不同句子的原因。将数据存储在数据库中不是一种选择,因为应用程序需要同时处理整个结构。如果我不得不继续将所有数据写入数据库,它将进一步减慢该过程。

谢谢

【问题讨论】:

  • 补充Marc的评论,为什么不存储在数据库中,让它管理内存分页? NOTE" 抱歉,Marc 我似乎编辑了您的评论,而不是添加新评论。我可以恢复吗?
  • 你真的需要一次记住所有的句子吗?
  • 为什么将它们存储为树?您的应用的目的是什么?
  • @Mitch - 我想你吃了我的评论;-p 但基本上我说:你真的需要内存中的所有数据吗?我在想可能有一种流媒体方式来做到这一点......
  • @Spi1988 - 感谢您抽出宝贵时间提供有关这方面的帮助的反馈。这对网站真的很有用,考虑到将来可能会访问这个问题的人。干杯。

标签: c# memory-management


【解决方案1】:

你用什么做钥匙?你从哪里得到数据?如果这些是单词(不是完整的句子),我想知道您是否有很多 重复 键(具有相同基本值的不同 string 实例),在在这种情况下,您可能会受益于实现本地内部人员以重用这些值(并让临时副本被垃圾收集)。

public sealed class StringCache {
    private readonly Dictionary<string,string> values
        = new Dictionary<string,string>(StringComparer.Ordinal);
    public string this[string value] {
        get {
            string cached;
            if (!values.TryGetValue(value, out cached)) {
                values.Add(value, value);
                cached = value;
            }
            return cached;
        }
    }
}

在构建树时将其实例化,并使用(当您认为某个值可能重复时):

StringCache cache = new StringCache(); // re-use this instance while building
                                       // your tree
...
string s = ... // whatever (from reading your input)
s = cache[s];

【讨论】:

  • 毫无疑问,这将减少内存需求。远远少于 400 万个单词 - 接近 100k。让他们实习会有很大的不同。
  • @Marc:有什么理由不使用 string.Intern()?
  • @Marc:我想我刚刚发现了原因!:在 CLR 终止之前,内存可能不会被释放......
  • 谢谢,我觉得这对我很有帮助。
  • 我实现了你的解决方案,内存使用率提高了很多。在使用您的方法之前,当系统达到整个过程的 1/3 时,系统会填满我所有的 2GB RAM。现在它只用了大约。整个过程需要 200MB 的 RAM。收益是巨大的,因为我有很多重复的字符串。谢谢你们的帮助
【解决方案2】:

Dictionary 类型本身会消耗大量内存。您是否考虑过改用List&lt;KeyValuePair&lt;string, TreeNode&gt;&gt;?与通用 Dictionary 相比,通用 List 每个实例使用的内存要少得多。

当然,使用 List 而不是 Dictionary 的限制是您无法通过字符串自动索引。这将是时间和空间之间的明显权衡。如果列表很短,它甚至可能比字典更快(大约 10 个键的线性搜索通常比哈希表搜索更快)。即使至少大多数列表很短,它仍然可能是一个很大的改进(例如,如果 95% 的列表有 10 个或更少的项目,而另外 5% 的列表最多可能有 100 个项目)。

您甚至可以使用Collection&lt;KeyValuePair&lt;string, TreeNode&gt;&gt;,它使用的内存比List&lt;T&gt; 还要少。

【讨论】:

  • 所以......为此目的有一个 HybridDictionary。它从一个列表开始,然后变成一个字典。
  • 是的,有 HybridDictionary,但即使它也有一些与之相关的额外费用。 HybridDictionary 一开始使用大约 32 字节的内存,Dictionary 大约 44 字节,List 大约 16 字节,Collection 大约 8 字节。(这不包括 CLR 开销,假设为 32 位。)
  • 我会先试用 HybridDictionary,因为如果可能的话,我想保留字符串索引。
【解决方案3】:

如果您的要求是性能,并且您感觉好像需要内存中的所有单词,那么我建议您使用字符串数组来包含所有单词。然后将所有索引存储到排序的二叉树中。

【讨论】:

    【解决方案4】:

    你能将每个单词映射到一个 int 吗?这样你就有了一个 int 到 string 的映射,其中包含唯一的英语单词和一个包含如下句子的树结构:

    class TreeNode
    {
        protected int word;
        protected Dictionary<int, TreeNode> children;
    }
    
    Dictionary<string, int> _AllWords;
    

    现在_AllWords 集合对于根据键查找单词并不是最佳选择。您可能想要的是一个多键列表,您可以在其中基于键和值进行快速查找。 CodeProject 有一篇关于此的文章。

    【讨论】:

    • 请注意,在 x86 上,这实际上与我给出的“内部”建议相同,但不需要在 int 键和字符串值之间进行额外的查找。相反,每个 int 引用本身。
    【解决方案5】:

    这对于您的情况可能有点过分,但您可以将节点存储在磁盘上的文件中,并使用B-Tree 实现来最大化 IO 性能。这是大多数数据库在内部使用的,因为内存中存储的数据太多了。

    【讨论】:

      【解决方案6】:

      需要考虑的几点。

      1. 当您初始化字典 时,传入您需要的最大项目数。这将使它在启动时分配足够的桶。默认是使用 0 个存储桶进行初始化,其计算结果为 3(素数)。添加更多项目后,字典必须重新初始化并将所有项目复制到新的更大存储中。如果您的程序从不空闲,则 GC 不会收集旧字典。
      2. 您可以通过对字符串进行编码来节省空间。字符串将在内存中每个字符使用两个字节。使用一些辅助函数,您可以拥有这样的类:
          class TreeNode
          {
              protected byte[] word;
              protected Dictionary<byte[], TreeNode> children;
      
              public string Word
              {
                  get { return Encoding.UTF8.GetString(word); }
                  set { word = Encoding.UTF8.GetBytes(value); }
              }
      
              public TreeNode GetChildByKey( string key )
              {
                  TreeNode node;
                  if(children.TryGetValue( Encoding.UTF8.GetBytes(key), out node  ))
                  {
                      return node;
                  }
                  return null;
              }
          }

      [编辑] 我忘了你还需要一个新的 byte[] 键比较器。

      var children = new Dictonary<string,TreeNode>(new ByteArrayComparer);
      
      public class ByteArrayComparer : IEqualityComparer<byte[]>
      {
          public bool Equals(byte[] x, byte[] y)
          {
              if (x.Length != y.Length)
                  return false;
      
              for (int i = 0; i < x.Length; i++)
              {
                  if (x[i] != y[i])
                      return false;
              }
      
              return true;
          }
      
          public int GetHashCode(byte[] a)
          {
              return a[0] | (int)a[1] << 8 | (int)a[2] << 16 | (int)a[3] << 24;
          }
      }
      

      【讨论】:

      • 为了完整起见 - 编码可能在这里占有一席之地,因为问题与“英语句子”有关,但对于某些文化来说,这实际上可能会使字符串使用的内存增加一倍。
      • 这是一个很好的观察结果,我实际上没有考虑过。我已经习惯了在西方字符集中工作。在进行编码之前,请测试一下是否有帮助。使用可变字节结构也可能有所帮助,特别是在字符串较长的情况下。但在采用压缩方式之前,您应该重新考虑手头的整个问题。
      【解决方案7】:

      显着减少内存使用的唯一方法是不要将句子留在记忆中。

      你想完成什么?你为什么要造树?如果您正在计算某些内容,请在阅读时计算并丢弃字符串。如果您正在构建图表(即分析句子和/或单词之间的关系),请尝试枚举句子和单词,因为它们可以是唯一的/关键通过那个 id。改为在内存中使用该 id。

      我希望这会有所帮助。

      【讨论】:

      • 我很高兴地报告,有种方法可以显着减少内存使用量。
      【解决方案8】:

      要减少内存占用,您应该查找Sequential Data Cache

      您使用的集合可以减少内存占用。 (集合项必须标有[Serializable])

      您甚至可以通过传递 deleteOnClose:false 参数使集合永久化

      样本

      using (var c = SequentialDataCache<TreeNode>.Initialize(deleteOnClose: false))
              {
                  //add items to collection
                  for (int i = 0; i < 1000; i++)
                  {
                      var treeNode = new TreeNode()
                                         {
                                             Word = string.Format("Word{0}", i),
                                             Children = new Dictionary<string, TreeNode>()
                                         };
                      for (int j = 0; j < 100; j++)
                      {
                          var child = new TreeNode() { Word = string.Format("Word{0}", j) };
                          treeNode.Children.Add(string.Format("key{0}{1}", i, j), child);
                      }
                      c.Add(treeNode);
                  }
      
                  //assert query
                  Assert.AreEqual("Word0", c[0].Word);
                  Assert.AreEqual("Word1", c[0].Children["key01"].Word);
                  Assert.AreEqual("Word100", c[100].Word);
              }
      

      还有 TreeNode...

          [Serializable]
          class TreeNode
          {
              private string word;
              private Dictionary<string, TreeNode> children;
      
              public string Word
              {
                  get { return word; }
                  set { word = value; }
              }
      
              public Dictionary<string, TreeNode> Children
              {
                  get { return children; }
                  set { children = value; }
              }
          }
      

      【讨论】:

        【解决方案9】:

        很好的问题,以及一些很好的答案。我学到了很多。 StringCache 的想法值得研究。

        我想回应“我不能使用数据库,因为我需要它全部在内存中”这一点。在很多情况下,数据库实际上是最好的解决方案。

        考虑一个强大的 SQL 数据库引擎(我是 MSSQL 人):

        • 可以容纳更多的数据——磁盘的大小而不是内存或交换空间的大小。 (SQL 数据库还可以利用单独机器上的内存和磁盘,从而增加可用空间并权衡网络延迟。)
        • 索引数据以便快速检索
        • 动态缓存最常使用的数据,并在内存压力需要时释放使用较少的数据。
        • 使用一个大型团队多年来开发的存储、检索和缓存算法,并经过调整以动态适应各种情况。

        动态缓存对于这个解决方案集可能是一个巨大的好处。假设您的语料库仅包含“正常”句子,则单词分布将不均匀。最频繁的词将比最不频繁的词更频繁地访问几个数量级。常用词也很可能很早就被添加到字典中,因此将被紧密地存储在数据库中。一个好的 SQL 引擎会将最常用的块缓存在内存中,这自然有利于您描述的那种搜索。

        混合解决方案可能如下所示:

        • 具有适当索引的表

          create table myWords (wordKey int identity, word varchar(50))
          create unique index iword 
            on myWords(word)  -- used for adds and retrieval
          create unique index iwordKey 
            on myWords(wordKey) -- used for mapping keys back to words
          
        • 用于添加/查找单词的存储过程。存储过程方便地返回一个 int。

          create procedure addWord (@word varchar(50))
          as
          begin
            declare @wordKey int, @rows int
            insert myWords (word)
              select @word
              where not exists (select 1 from myWords where word = @word)
            select @wordKey = @@identity, @rows = @@rowcount
            if @rows = 0
            begin
              select @wordKey = wordKey
                from myWords
                where word = @word
            end
            return @wordKey
          end
          
        • 应用程序将单词添加到数据库中,仅使用 wordKey 值在内存中构建树。

        • 搜索匹配的句子将从查询开始以获取相关单词的 wordKey 值,然后分析树,收集构建完整句子所需的 wordKey,最后使用第二个查询检索这些单词。

        您可以牺牲一点构建数据库的速度来进一步优化缓存最常用词的好处。

        1. 向表中添加一个字段 (usageCount int)。插入将其设置为 1,更新递增。
        2. 仅使用单词的索引,从您的语料库中填充字典表
        3. 在usageCount (desc) 上添加一个聚集索引,该索引将重新组织以使最常用的单词靠近在一起。 (也许再放弃它 - 好工作已经完成。)
        4. 构建你的树。

        即使你的语料库在未来有所增长,词频也不太可能变化到足以影响效率。

        【讨论】:

          猜你喜欢
          • 2016-11-09
          • 2012-03-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多