首页 > 解决方案 > 在发布版本中的 foreach 迭代之间释放内存?

问题描述

更新 显然,最初的问题令人困惑,所以我会尽量简化它。

我正在使用一个复杂的算法,它提供一个对象列表(比方说一家公司)。
对于这些公司中的每一个,我都必须加载大量数据(比如说员工列表)。

public class Company
{
    public string Name { get; set; } = "";
    public List<Employee> EmployeeList { get; set; } = new List<Employee>();
}

public class Employee
{
    public string FirstName { get; set; } = "Random first name";
    public string LastName { get; set; } = "Random last name";
}

public MemoryTest()
{
    //Simulate the complex algorithme... 
    //I can't change how I get that list and my question ain't about this part.
    List<Company> companyList = new List<Company>();
    for (int i = 0; i < 50000; i++)
    {
        companyList.Add(new Company() {  Name = "Random company name " + i });
    }

    //Simulate the details loading.  This is where the memory gets filled            
    foreach (Company company in companyList)
    {
        company.EmployeeList.AddRange(new Employee[25000]);
        //Do some calculation and save to DB...
    }
}

这段代码的问题是每次迭代期间分配的内存直到循环结束才会被释放。

阅读完这篇文章后,我希望 JIT 能够确定在迭代后不会使用公司引用,因为 companyList 没有在 foreach 之外使用:

在发布版本中,JIT 能够查看程序结构以计算出该方法可以使用变量的执行过程中的最后一点,并在不再需要时将其丢弃。

...但遗憾的是,JIT 并没有推断出那么远。

为了尽可能地使用一些内存,我的问题如下:有没有办法通过一个集合循环并在每次迭代之间删除对元素的引用?

如果您不想与公司/员工一起工作,这是一个更通用的示例

Dictionary<int, List<string>> dict = new Dictionary<int, List<string>>();

for (int i = 0; i < 100000; i++)
{
    dict.Add(i, new List<string>());
}

foreach (var item in dict)
{
    item.Value.AddRange(new string[25000]);
}

标签: c#foreachgarbage-collection

解决方案


我将在这里修改我的原始答案。不要打败众所周知的死马,但我只想强调,如果您必须从集合中弹出项目以防止集合在内存中扎根对象,那么几乎可以肯定您的设计存在一些问题您的应用程序。可能在某些情况下这是一个很棒的设计,但我经常说不是。

让我们以 Smurf 的公司场景为例,稍微改变一下,让它完全不保留集合中的大对象。

我将忽略公司对象的字典。它从未用作字典,仅用作集合。同样重要的是要注意字典中的内容并不是一个完整的公司对象。我们必须使用原始的公司对象来检索额外的数据。我们通常称之为钥匙。所以代替那个字典,我们将有一个键流。这是关键对象:

public class CompanyKey
{ }

以及生成密钥的数据源。这实际上可能是 Smurfs 字典,但为了我们的目的,我们将使它成为一个迭代器方法。这样一来,就没有任何东西可以将这些东西扎根于记忆中。如果键很小,那么它并不重要,但如果你不需要它,最好不要使用集合。

public class CompanyKeySource
{
    public IEnumerable<CompanyKey> GetKeys()
    {
        for(int i =0;i < 10;++i)
            yield return new CompanyKey();
    }
}

这是实际的公司对象:

public class Company
{
    public EmployeeData Employees { get; set; }
}

还有大量的数据。那是在员工对象中。

public class Employee
{
    public string[] LotOfData { get; set; }
}

最后,我们需要将大量数据加载到公司对象中的东西。这通常是某种类型的存储库:

public class CompanyDataRepository
{
    public IEnumerable<Company> GetCompanyDetails(IEnumerable<CompanyKey> keys)
    {
        foreach (var key in keys)
        {
            yield return new Company() { Employees = GetEmployees(key) };
        }
    }

    public EmployeeData GetEmployees(CompanyKey key) =>
        new EmployeeData() { LotOfData = new string[2500] };
}

现在我们将所有内容连接在一起并迭代我们的公司实例。

    static void Main(string[] _)
    {
        CompanyDataRepository repository = new CompanyDataRepository();
        CompanyKeySource keySource = new CompanyKeySource();

        var keys = keySource.GetKeys();

        foreach (var company in repository.GetCompanyDetails(keys))
        {
            // do whatever it is you're doing with your companies...
        }
    }

现在无需从字典中弹出项目以将它们排除在内存之外。大量数据用于需要它们的地方,然后可以立即进行收集。


推荐阅读