【问题标题】:Are there any tricks to consume less memory when needing to work with a large in memory collection?当需要处理大量内存集合时,是否有任何技巧可以减少内存消耗?
【发布时间】:2019-05-25 04:53:10
【问题描述】:

我的服务器上的 RAM 数量有限,但我需要在控制台程序的内存中处理大量数据。是否有任何技巧可以让我仍然获得相同的最终结果,但不需要那么多 RAM

对于这个例子,我在一个字符串列表中有 1 亿个电子邮件地址。我需要找出我与之比较的任何新电子邮件是否已经存在。如果是这样,请添加它们。如果没有,请不要添加它们。所以我们总是有一个唯一的电子邮件列表,没有重复。

在此示例中,1 亿封电子邮件需要大约 17GB 的 RAM。

您是否知道任何技巧或技巧可以减少所需的 RAM 量以至少仍然能够执行“它是否存在于列表集合中?”比较? - 想到的示例类型:例如不同类型的集合,或自定义的第三方引用的软件工具,可压缩内存中的数据,但您仍然可以对该数据进行排序或比较,或者可能是基于文件的数据库系统,它使用相同数量的数据上的内存要少得多。

我编写了代码来演示如何以正常方式执行此操作,从而消耗 17GB 的 RAM。

using System;
using System.Collections.Generic;
using System.Linq;

namespace NewProgram
{
    class Program
    {
        public static List<string> emails = new List<string>(); 

        public static void Main(string[] args)
        {
            LoadAllEmails();

            Console.WriteLine(emails.Count() + " total emails"); //100000000 total emails

            AddEmailsThatDontExistInMasterList(
                new List<string>()
                {
                "something@test.com", //does not already exist, so it will be added to list
                "testingfirst.testinglast"+ (1234567).ToString() + "@testingdomain.com", //should already exist, won't be added
                "testingfirst.testinglast"+ (3333335).ToString() + "@testingdomain.com", //should already exist, won't be added
                "something2@test.com", //does not already exist, so it will be added to list
                "testingfirst.testinglast"+ (8765432).ToString() + "@testingdomain.com", //should already exist, won't be added
                });

            Console.WriteLine(emails.Count() + " total emails after"); //100000002 total emails

            Console.ReadLine();
        }


        public static void LoadAllEmails()
        {
            for (int i = 0; i < 100000000; i++)  //100,000,000 emails = approximately 17GB of memory
            {
                emails.Add("testingfirst.testinglast" + i.ToString() + "@testingdomain.com");
            }
        }

        public static void AddEmailsThatDontExistInMasterList(List<string> newEmails)
        {
            foreach (string email in newEmails)
            {
                if (emails.Contains(email) == false)
                {
                    emails.Add(email);
                }
            }
        }
    }
}

在将 100,000,000 封电子邮件添加到“电子邮件”集合后,它会在添加到其中的新列表中再查看 5 封电子邮件。将添加 2 个,不会添加 3 个,因为它们已经在列表中。完成时的总数是集合中的 100,000,002 封电子邮件。这只是为了证明我的最终目标是能够与现有集合进行比较,以查看一个值是否重复或已经存在于该集合中,一个非常大的数据集合。另一个目标是将总消耗的 RAM 从 17 GB 降低到更小。

【问题讨论】:

  • 你可以使用数据库。
  • 或者,从排序列表中工作。然后你的内存需求下降到 O(1)。
  • 数据库 + 电子邮件地址索引 = 内存使用量小,查找速度快。请解释为什么列表需要在内存中而不是这样做。或者只是向您的服务器添加另外 32 GB 的 RAM :)
  • 如果您仍在使用内存路由,您还可以考虑使用 UTF-8 编码存储字符串,这应该可以对大多数存储地址进行 2:1 压缩,因为将编码许多字符如果使用默认 (UTF-16) 编码,则为单个字节而不是 16 位。
  • 是否有特定的 NFR 要求您在内存中执行此操作?显而易见的答案是使用数据库。如果这还不够快,请使用更大的数据库服务器。如果这还不够快,请使用数据库集群。

标签: c# memory-management out-of-memory


【解决方案1】:

选项 1 使用三叉树

这种数据结构是一种在内存中存储单词的有效方法。它高度压缩且搜索速度快。

选项 2 使用内存中的哈希和磁盘文件

在内存中只保留每封电子邮件的哈希值。如果您在哈希表中获得成功,请查看磁盘。

选项 3 使用布隆过滤器和磁盘文件

https://llimllib.github.io/bloomfilter-tutorial/

【讨论】:

  • 您是指Ternary Tree 还是Trie
  • @Enigmativity 我的意思是三叉树,但也可以使用 trie。我的假设是电子邮件地址中的字母分布使得三叉树的空间效率更高。为了在域名上获得更多通用性,将电子邮件一分为二甚至可能是有意义的。
  • 是的,我认为反转电子邮件实际上可能会导致最有效的 Trie。
  • 不要使用默认的 .NET 字符串哈希码来引用保存在磁盘上的文件。如果您重新启动程序,或启动新的应用程序域,字符串可能会产生不同的哈希值。
  • @IanMercer 在针对已知不存在的电子邮件进行测试时尝试的 1,000,000 封随机电子邮件中有 34 个误报(这是可以接受的,我可以双重验证)。测试 1,000,000 封已知已存在的随机电子邮件时出现 0 个误报。与所有 200 万封电子邮件进行比较的时间不到 1 秒。布隆过滤器很棒!
【解决方案2】:

您似乎在暗示您在一个文本文件中拥有 1 亿个电子邮件地址。我认为不需要将整个文件加载到内存中并循环遍历它;使用流阅读器并逐行阅读。对于每一行,检查刚刚读取的电子邮件地址是否在您要导入的 10 个列表中,如果是,则将其从导入列表中删除

在该过程结束时,您会将导入列表减少到仅不在大文件中的那些地址,并且您一次读取的内容永远不会超过一行(阅读器会缓存一些少量的千字节)

改编自微软的示例集合:

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-read-a-text-file-one-line-at-a-time

string line;  
string[] emailsToImport = "a@b.com c@d.com".Split();

// Read the file and process it line by line.  
System.IO.StreamReader file =   
  new System.IO.StreamReader(@"c:\100million.txt");  
while((line = file.ReadLine()) != null)  
{  
    for(int i = 0; i < emailsToImport.Length; i++){
      if(emailsToImport[i] == line)
        emailsToImport[i] = null;
    }
}  

file.Close();  
System.Console.WriteLine("new emails remaining to import: {0} ", string.Join(",", emailsToImport));  

这是一个不区分大小写的快速且非常肮脏的示例;它旨在作为一个概念的简单解释,而不是生产代码

我做了以下假设:

您的服务器具有少量内存(例如 4gb),并且您很少需要(例如每 5 分钟一次)将少量电子邮件地址(例如 10 个)添加到包含 1 亿个地址的大型列表中,确保新地址不重复

逐行读取文件,将每一行与所有 10 个新地址进行比较,删除任何已知的地址。在读取文件结束时,一旦您开始使用多达 N 个地址,您知道这些地址不存在于主列表中。

我断言,在这种情况下,您的原始陈述“我需要在内存中处理大量数据”可以在磁盘上处理

【讨论】:

  • 您建议使用O(N^2) 解决方案。我认为这并不理想。
  • 请解释一下这是N^2。请详细说明您对“理想”的定义。请随时提出一个“理想”的答案..
  • OP 的代码显示他正在调用foreach (string email in newEmails),然后继续检查newEmails 中每封电子邮件的所有电子邮件列表(读取文件)。那是O(n^2)。理想情况下,它不需要每次都重新读取整个文件。
  • 如果您想与 OP 交谈,您应该在问题下发布您的 cmets
  • 所以当您说“OP 的代码”时,您的意思是我是 OP?你很混乱。此代码读取文件一次,并在其中查找少量电子邮件。它不打算对 1 亿封电子邮件进行重复数据删除。它不打算将 100M 电子邮件列表唯一地导入现有的 100M 电子邮件列表。这是一个简单的解决方案,旨在通过将大部分文件留在磁盘上来最小化内存占用,适合在内存极低的服务器上使用。我们都欢迎你发表一些你自己的智慧,而不是仅仅告诉其他人你认为他们做错了什么..
【解决方案3】:

要检查项目是否不在非常大的列表中,您可以使用Bloom Filter。它是散列表的概括,它为每个输入字符串生成一个散列列表,与多个桶中的散列表不同。因此,如果您有一个哈希值冲突,它仍然可以通过检查其他哈希是否曾经添加过确切的字符串来解决。

您仍然可以有误报( filter.Contains("xxxx") 返回 true,尽管它从未添加到列表中)但绝不会出现误报。

通过调整容量,您可以配置错误率。如果您的过滤器可以承受很小的误报率,那么这个应该很好。

例如,查看this one

我已经尝试了几次。大多数实现在它们的节点类中使用引用,这是非常低效的内存。 SO上的一个似乎相当不错:How to create a trie in c#。这个可以节省大约。与普通列表相比 30%。

我认为除了数据结构之外,您还需要查看总体目标。我猜您的实际数据不是电子邮件地址,因为垃圾邮件过滤器是一个长期解决的问题。但是为了玩弄您的示例,您可以利用数据中的结构。您有一个包含名称和域的大型列表。要做的第一件事是将您的数据拆分为仅包含一个域的邮件地址的较小文件。然后按名称排序并为每个域创建一个索引,其中每个字母的文件起始位置存储在其标题中。

当有新邮件到达时,您可以先使用布隆过滤器进行快速预检查。如果它说它是干净的,那么您已经完成了所有案例的 99.5%。为什么是 99.5%?您可以通过计算要花费多少内存来获得此准确性,将您的布隆过滤器配置为具有此属性。

从系统的角度来看,这很棒,因为您现在可以有意识地决定要为此任务花费多少内存/CPU/磁盘资源。

当您命中时,您可以打开该域的索引文件,直接跳转到已排序的邮件地址并开始阅读地址,直到您命中了坏人或者您超出了字母排序顺序并且您可以停止阅读。

如果您拥有更多域,就知道如何变得更聪明。因为您知道公司的有效发件人数量非常有限,您可以创建一个更小的有效发件人检查白名单。如果发件人在白名单上,您可以跳过其他检查。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-04-05
    • 1970-01-01
    • 1970-01-01
    • 2023-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多