需要处理大量内存时,是否有任何技巧可以消耗更少的内存?

时间:2019-05-25 04:53:11

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

我的服务器上的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减小到更小。

3 个答案:

答案 0 :(得分:3)

选项1使用三叉树

此数据结构是一种将字存储在内存中的有效方法。高度压缩且搜索迅速。

选项2使用内存散列和磁盘文件

仅保留内存中每封电子邮件的哈希值。如果您在哈希表中遇到问题,请在磁盘上查看。

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

请参见https://llimllib.github.io/bloomfilter-tutorial/

答案 1 :(得分:1)

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

在此过程结束时,您将已将导入列表缩减为不包含在大文件中的那些地址,并且您一次不会读取多行内容(阅读器将缓存少量的行)。千字节)

改编自Microsoft的示例集合:

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个)添加到大量的100M地址列表中,确保不会重复新地址

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

我断言,在这种情况下,您原来的声明“我需要在内存中处理大量数据”可以在磁盘上使用

答案 2 :(得分:0)

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

您仍然可以有误报(filter.Contains(“ xxxx”)虽然从未添加到列表中,但返回true),但也不会有误报。

通过调整容量,您可以配置错误率。如果您的过滤器可以以很小的误报率生存,那么这个应该是不错的选择。

有关示例,请查看this one

我尝试了几次。大多数实现都在其节点类中使用引用,这在内存方面效率极低。 SO上的一个似乎很不错:How to create a trie in c#。这个节省约。与普通清单相比,降低了30%。

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

当收到新邮件时,您可以先使用Bloom Filter进行快速的预检查。如果它说的很干净,您已经完成所有病例的99.5%。为什么是99.5%?您可以通过计算要花费多少内存来获得此精度来将Bloom Filter配置为具有此属性。

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

点击后,您可以打开该域的索引文件,直接跳至排序后的邮件地址,然后开始阅读地址,直到遇到坏人或超过字母排序顺序后即可停止阅读。

如果您拥有更多领域,请知道如何变得更聪明。因为您知道公司的有效发件人数量受到很大限制,所以您可以创建一个较小的有效发件人检查白名单。如果发件人在白名单上,您可以跳过其他检查。