C#字典和高效内存使用

9
我有一个工具可以比较两个csv文件,然后将每个单元格分成6个桶之一。基本上,它使用快速csv阅读器(来源:http://www.codeproject.com/KB/database/CsvReader.aspx)读取csv文件,然后根据用户提供的键创建与每个文件相关的字典。然后,我通过迭代字典比较值并编写结果csv文件。

虽然它非常快,但在内存使用方面效率非常低。我的电脑上无法比较超过150 MB的文件,而物理内存为3 GB。

以下是读取预期文件的代码片段。在此片段结束时,任务管理器中的内存使用量接近500 MB。

// Read Expected
long rowNumExp;
System.IO.StreamReader readerStreamExp = new System.IO.StreamReader(@expFile);
SortedDictionary<string, string[]> dictExp = new SortedDictionary<string, string[]>();
List<string[]> listDupExp = new List<string[]>();
using (CsvReader readerCSVExp = new CsvReader(readerStreamExp, hasHeaders, 4096))
{
    readerCSVExp.SkipEmptyLines = false;
    readerCSVExp.DefaultParseErrorAction = ParseErrorAction.ThrowException;
    readerCSVExp.MissingFieldAction = MissingFieldAction.ParseError;
    fieldCountExp = readerCSVExp.FieldCount;                
    string keyExp;
    string[] rowExp = null;
    while (readerCSVExp.ReadNextRecord())
    {
        if (hasHeaders == true)
        {
            rowNumExp = readerCSVExp.CurrentRecordIndex + 2;
        }
        else
        {
            rowNumExp = readerCSVExp.CurrentRecordIndex + 1;
        }
        try
        {
            rowExp = new string[fieldCount + 1];                    
        }
        catch (Exception exExpOutOfMemory)
        {
            MessageBox.Show(exExpOutOfMemory.Message);
            Environment.Exit(1);
        }                
        keyExp = readerCSVExp[keyColumns[0] - 1];
        for (int i = 1; i < keyColumns.Length; i++)
        {
            keyExp = keyExp + "|" + readerCSVExp[i - 1];
        }
        try
        {
            readerCSVExp.CopyCurrentRecordTo(rowExp);
        }
        catch (Exception exExpCSVOutOfMemory)
        {
            MessageBox.Show(exExpCSVOutOfMemory.Message);
            Environment.Exit(1);
        }
        try
        {
            rowExp[fieldCount] = rowNumExp.ToString();
        }
        catch (Exception exExpRowNumOutOfMemory)
        {
            MessageBox.Show(exExpRowNumOutOfMemory.Message);
            Environment.Exit(1);
        }
        // Dedup Expected                        
        if (!(dictExp.ContainsKey(keyExp)))
        {
            dictExp.Add(keyExp, rowExp);                        
        }
        else
        {
            listDupExp.Add(rowExp);
        }                    
    }                
    logFile.WriteLine("Done Reading Expected File at " + DateTime.Now);
    Console.WriteLine("Done Reading Expected File at " + DateTime.Now + "\r\n");
    logFile.WriteLine("Done Creating Expected Dictionary at " + DateTime.Now);
    logFile.WriteLine("Done Identifying Expected Duplicates at " + DateTime.Now + "\r\n");                
}

有没有什么方法可以使它更加节省内存?我需要做一些不同的事情来消耗更少的内存吗?

欢迎提出任何想法。

感谢大家的反馈。

我已经按照建议进行了更改,将行的索引而不是行本身存储在字典中。

这里是具有新实现的相同代码片段。

// Read Expected
        long rowNumExp;
        SortedDictionary<string, long> dictExp = new SortedDictionary<string, long>();
        System.Text.StringBuilder keyExp = new System.Text.StringBuilder();
        while (readerCSVExp.ReadNextRecord())
        {
            if (hasHeaders == true)
            {
                rowNumExp = readerCSVExp.CurrentRecordIndex + 2;
            }
            else
            {
                rowNumExp = readerCSVExp.CurrentRecordIndex + 1;
            }
            for (int i = 0; i < keyColumns.Length - 1; i++)
            {
                keyExp.Append(readerCSVExp[keyColumns[i] - 1]);
                keyExp.Append("|");
            }
            keyExp.Append(readerCSVExp[keyColumns[keyColumns.Length - 1] - 1]);
            // Dedup Expected                       
            if (!(dictExp.ContainsKey(keyExp.ToString())))
            {
                dictExp.Add(keyExp.ToString(), rowNumExp);
            }
            else
            {
                // Process Expected Duplicates          
                string dupExp;
                for (int i = 0; i < fieldCount; i++)
                {
                    if (i >= fieldCountExp)
                    {
                        dupExp = null;
                    }
                    else
                    {
                        dupExp = readerCSVExp[i];
                    }
                    foreach (int keyColumn in keyColumns)
                    {
                        if (i == keyColumn - 1)
                        {
                            resultCell = "duplicateEXP: '" + dupExp + "'";
                            resultCell = CreateCSVField(resultCell);
                            resultsFile.Write(resultCell);
                            comSumCol = comSumCol + 1;
                            countDuplicateExp = countDuplicateExp + 1;
                        }
                        else
                        {
                            if (checkPTColumns(i + 1, passthroughColumns) == false)
                            {
                                resultCell = "'" + dupExp + "'";
                                resultCell = CreateCSVField(resultCell);
                                resultsFile.Write(resultCell);
                                countDuplicateExp = countDuplicateExp + 1;
                            }
                            else
                            {
                                resultCell = "PASSTHROUGH duplicateEXP: '" + dupExp + "'";
                                resultCell = CreateCSVField(resultCell);
                                resultsFile.Write(resultCell);
                            }
                            comSumCol = comSumCol + 1;
                        }
                    }
                    if (comSumCol <= fieldCount)
                    {
                        resultsFile.Write(csComma);
                    }
                }
                if (comSumCol == fieldCount + 1)
                {
                    resultsFile.Write(csComma + rowNumExp);
                    comSumCol = comSumCol + 1;
                }
                if (comSumCol == fieldCount + 2)
                {
                    resultsFile.Write(csComma);
                    comSumCol = comSumCol + 1;
                }
                if (comSumCol > fieldCount + 2)
                {
                    comSumRow = comSumRow + 1;
                    resultsFile.Write(csCrLf);
                    comSumCol = 1;
                }
            }
            keyExp.Clear();
        }
        logFile.WriteLine("Done Reading Expected File at " + DateTime.Now + "\r\n");
        Console.WriteLine("Done Reading Expected File at " + DateTime.Now + "\r\n");
        logFile.WriteLine("Done Analyzing Expected Duplicates at " + DateTime.Now + "\r\n");
        Console.WriteLine("Done Analyzing Expected Duplicates at " + DateTime.Now + "\r\n");
        logFile.Flush();

然而,问题在于我需要将两个数据集都存储在内存中。我实际上会遍历这两个字典,根据键寻找匹配、不匹配、重复和丢失的数据。
使用存储行索引的方法,我仍然使用了大量内存,因为对于动态访问,我现在必须使用缓存版本的csv读取器。因此,尽管字典现在要小得多,但数据的缓存弥补了节省的空间,我最终仍然使用了类似的内存。
希望我说得清楚... :)
一种选择是完全摆脱字典,只是循环遍历这两个文件,但不确定与比较两个字典相比性能是否会更快。
非常感谢您提供任何意见。

你能否将文件中的记录位置缓存在缓存中,以便稍后获取记录,而不是缓存csv阅读器?当您通过键迭代字典查找辍学等时,您是在查看实际数据还是仅查看键? - Sam Holder
你尝试过在将字符串放入字典之前对其进行内部化吗?这样做有什么区别吗?这些操作是否有助于减少内存使用? - Sam Holder
3个回答

7
你可以用 StringBuilder 替换 keyExp。在循环中重新分配字符串会导致不断分配更多的内存,因为字符串是不可变的。
StringBuilder keyExp = new StringBuilder();
...
    keyExp.Append("|" + readerCSVExp[i - 1]) ;
... 

许多字符串都是相同的吗?您可以尝试对它们进行内部化处理,这样任何相同的字符串将共享同一内存,而不是复制...

rowExp[fieldCount] = String.Intern(rowNumExp.ToString()); 

// Dedup Expected               
string internedKey = (String.Intern(keyExp.ToString()));        
if (!(dictExp.ContainsKey(internedKey)))
{
   dictExp.Add(internedKey, rowExp);                        
}
else
{
   listDupExp.Add(rowExp);
}  

我不确定代码的具体工作方式,但是除此之外,我认为你不需要在字典中保留rowExp,可以保留其他内容,比如一个数字,并将rowExp写回另一个文件。这样做可能会节省最多的内存,因为它似乎是文件中的字符串数组,所以可能很大。如果你将它写入文件并保留在文件中的数字,则在将来需要处理时可以再次访问它。如果你将文件中的偏移量保存为字典中的值,那么你就能够快速找到它。也许:)。

有趣的是,我一直以为编译器/解释器/即时编译器/某些东西会自动将字符串内部化,但这可能仅适用于在编译时已知相同的字符串吧。 - Davy8
1
@Davy8,没错。字符串驻留仅在从编译时常量创建的字符串上默认发生。 - Eric Lippert

3
告诉我如果我有任何错误。
上面的代码读取一个CSV文件并查找重复键。每一行都会进入两个集合之一,一个是有重复键的集合,另一个则没有。
这些行集合怎么处理?
它们被写入不同的文件吗?
如果是的话,就没有必要将非唯一行存储在列表中,当你找到它们时,直接将它们写入文件即可。
当你发现重复项时,没有必要存储整行数据,只需存储键,并将该行写入文件(如果你想将其分开存储,则显然是不同的文件)。
如果你需要对不同的集合进行进一步处理,那么不要存储整行数据,而是存储行号。然后,在你对行执行任何操作时,你就可以使用必要的行号来获取该行数据。
注意:你可以存储行偏移量而不是行号。然后,如果需要,你可以随机访问文件并读取行。
如果你有任何问题(或需要澄清),请在评论中提出,我会更新回答。我还会在这里待上几个小时。
编辑: 你可以通过不存储键而存储键的哈希值来进一步减少内存占用。如果你找到了重复项,可以在文件中查找该位置,重新读取该行并比较实际的键。

请查看我在上面编辑过的帖子中的回复。抱歉,不知道如何在评论中成功粘贴代码示例。 - user262102

2
如果您还没有像DotTrace这样的分析器来查看使用内存的对象,那么这将给您一个很好的优化方向。
从代码中可以得到一些想法:
您是否需要存储listDupExp?在我看来,使用list时您实际上将两个文件加载到内存中,因此2 x 150MB + 一些开销很容易接近任务管理器中的500MB。
其次,您能否在读取所有输入之前开始编写输出?我假设这很棘手,因为它看起来像是在写出所有输出项之前需要对它们进行排序,但这可能是您可以考虑的一些内容。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接