Почему я не могу использовать 4 ГБ оперативной памяти на моем компьютере для обработки менее 2 ГБ информации в C #? - PullRequest
6 голосов
/ 01 апреля 2009

Сценарий: более 1,5 ГБ текстовых и CSV-файлов, которые мне нужны для математической обработки. Я пытался использовать SQL Server Express, но загрузка информации, даже при импорте BULK, занимает очень много времени, и в идеале мне нужно иметь весь набор данных в памяти, чтобы уменьшить количество операций ввода-вывода жесткого диска.

Существует более 120 000 000 записей, но даже когда я пытаюсь отфильтровать информацию только в один столбец (в памяти), мое консольное приложение C # потребляет ~ 3,5 ГБ памяти для обработки всего 125 МБ (700 МБ фактически считываются) текста.

Кажется, что ссылки на строки и строковые массивы не собираются GC, даже после установки всех ссылок на null и инкапсуляции IDisposables с ключевым словом using.

Я думаю, что виновником является метод String.Split (), который создает новую строку для каждого значения, разделенного запятыми.

Вы можете предложить, чтобы я даже не читал ненужные * столбцы в массив строк, но это упускает из виду: как я могу поместить этот весь набор данных в память, чтобы я мог обработать его параллельно в C #?

Я мог бы оптимизировать статистические алгоритмы и координировать задачи с помощью сложного алгоритма планирования, но это то, что я надеялся сделать до того, как столкнулся с проблемами памяти, а не из-за.

Я включил полное консольное приложение, которое имитирует мою среду и должно помочь воспроизвести проблему.

Любая помощь приветствуется. Заранее спасибо.

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace InMemProcessingLeak
{
    class Program
    {
        static void Main(string[] args)
        {
            //Setup Test Environment. Uncomment Once
            //15000-20000 files would be more realistic
            //InMemoryProcessingLeak.GenerateTestDirectoryFilesAndColumns(3000, 3);
            //GC
            GC.Collect();
            //Demostrate Large Object Memory Allocation Problem (LOMAP)
            InMemoryProcessingLeak.SelectColumnFromAllFiles(3000, 2);
        }
    }

    class InMemoryProcessingLeak
    {
        public static List<string> SelectColumnFromAllFiles(int filesToSelect, int column)
        {
            List<string> allItems = new List<string>();
            int fileCount = filesToSelect;
            long fileSize, totalReadSize = 0;

            for (int i = 1; i <= fileCount; i++)
            {
                allItems.AddRange(SelectColumn(i, column, out fileSize));
                totalReadSize += fileSize;
                Console.Clear();
                Console.Out.WriteLine("Reading file {0:00000} of {1}", i, fileCount);
                Console.Out.WriteLine("Memory = {0}MB", GC.GetTotalMemory(false) / 1048576);
                Console.Out.WriteLine("Total Read = {0}MB", totalReadSize / 1048576);
            }
            Console.ReadLine();
            return allItems;

        }

        //reads a csv file and returns the values for a selected column
        private static List<string> SelectColumn(int fileNumber, int column, out long fileSize)
        {
            string fileIn;
            FileInfo file = new FileInfo(string.Format(@"MemLeakTestFiles/File{0:00000}.txt", fileNumber));
            fileSize = file.Length;
            using (System.IO.FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                using (System.IO.StreamReader sr = new System.IO.StreamReader(fs))
                {
                    fileIn = sr.ReadToEnd();
                }
            }

            string[] lineDelimiter = { "\n" };
            string[] allLines = fileIn.Split(lineDelimiter, StringSplitOptions.None);

            List<string> processedColumn = new List<string>();

            string current;
            for (int i = 0; i < allLines.Length - 1; i++)
            {
                current = GetColumnFromProcessedRow(allLines[i], column);
                processedColumn.Add(current);
            }

            for (int i = 0; i < lineDelimiter.Length; i++) //GC
            {
                lineDelimiter[i] = null;
            }
            lineDelimiter = null;

            for (int i = 0; i < allLines.Length; i++) //GC
            {
                allLines[i] = null;
            }
            allLines = null;
            current = null;

            return processedColumn;
        }

        //returns a row value from the selected comma separated string and column position
        private static string GetColumnFromProcessedRow(string line, int columnPosition)
        {
            string[] entireRow = line.Split(",".ToCharArray());
            string currentColumn = entireRow[columnPosition];
            //GC
            for (int i = 0; i < entireRow.Length; i++)
            {
                entireRow[i] = null;
            }
            entireRow = null;
            return currentColumn;
        }

        #region Generators
        public static void GenerateTestDirectoryFilesAndColumns(int filesToGenerate, int columnsToGenerate)
        {
            DirectoryInfo dirInfo = new DirectoryInfo("MemLeakTestFiles");
            if (!dirInfo.Exists)
            {
                dirInfo.Create();
            }
            Random seed = new Random();

            string[] columns = new string[columnsToGenerate];

            StringBuilder sb = new StringBuilder();
            for (int i = 1; i <= filesToGenerate; i++)
            {
                int rows = seed.Next(10, 8000);
                for (int j = 0; j < rows; j++)
                {
                    sb.Append(GenerateRow(seed, columnsToGenerate));
                }
                using (TextWriter tw = new StreamWriter(String.Format(@"{0}/File{1:00000}.txt", dirInfo, i)))
                {
                    tw.Write(sb.ToString());
                    tw.Flush();
                }
                sb.Remove(0, sb.Length);
                Console.Clear();
                Console.Out.WriteLine("Generating file {0:00000} of {1}", i, filesToGenerate);
            }
        }

        private static string GenerateString(Random seed)
        {
            StringBuilder sb = new StringBuilder();
            int characters = seed.Next(4, 12);
            for (int i = 0; i < characters; i++)
            {
                sb.Append(Convert.ToChar(Convert.ToInt32(Math.Floor(26 * seed.NextDouble() + 65))));
            }
            return sb.ToString();
        }

        private static string GenerateRow(Random seed, int columnsToGenerate)
        {
            StringBuilder sb = new StringBuilder();

            sb.Append(seed.Next());
            for (int i = 0; i < columnsToGenerate - 1; i++)
            {
                sb.Append(",");
                sb.Append(GenerateString(seed));
            }
            sb.Append("\n");

            return sb.ToString();
        }
        #endregion
    }
}

* Эти другие столбцы будут необходимы и будут доступны как последовательно, так и случайным образом в течение всего жизненного цикла программы, поэтому чтение с диска каждый раз приводит к огромным затратам.

** Замечания по среде: 4 ГБ DDR2 SDRAM 800, Core 2 Duo 2,5 ГГц, .NET Runtime 3.5 SP1, Vista 64.

Ответы [ 3 ]

14 голосов
/ 01 апреля 2009

Да, String.Split создает новый объект String для каждого «куска» - вот что он должен делать.

Теперь, имейте в виду, что строки в .NET являются Unicode (действительно UTF-16), и с учетом накладных расходов объекта стоимость строки в байтах составляет приблизительно 20 + 2*n, где n - это количество символов.

Это означает, что если у вас много маленьких строк, это займет много памяти по сравнению с размером текстовых данных. Например, строка из 80 символов, разделенная на строки 10 x 8 символов, займет в файле 80 байт, но 10 * (20 + 2 * 8) = 360 байт в памяти - увеличение в 4,5 раза!

Я сомневаюсь, что это проблема GC - и я бы посоветовал вам удалить дополнительные операторы, в которых переменные устанавливаются в нуль, когда в этом нет необходимости, - просто проблема наличия слишком большого количества данных.

То, что я предложил бы , заключается в том, что вы читаете файл построчно (используя TextReader.ReadLine() вместо TextReader.ReadToEnd()). Очевидно, что весь файл в памяти, если вам это не нужно, является расточительным.

3 голосов
/ 01 апреля 2009

Я бы посоветовал читать построчно, а не весь файл, или блок размером до 1-2 МБ.

Обновление:
Из комментариев Джона мне было любопытно, и я экспериментировал с 4 методами:

  • StreamReader.ReadLine (по умолчанию и пользовательский размер буфера),
  • StreamReader.ReadToEnd
  • Мой метод указан выше.

Чтение файла журнала размером 180 Мб:

  • ReadLine ms: 1937
  • Больший буфер ReadLine, ascii ms: 1926
  • ReadToEnd ms: 2151
  • Пользовательские мс: 1415

Пользовательский StreamReader был:

StreamReader streamReader = new StreamReader(fileStream, Encoding.Default, false, 16384)

Буфер StreamReader по умолчанию равен 1024.

Для потребления памяти (актуальный вопрос!) - ~ 800мб используется. И метод, который я даю, по-прежнему использует StringBuilder (который использует строку), поэтому не меньше расходует память.

2 голосов
/ 01 апреля 2009

Современные языки GC используют большие объемы дешевой оперативной памяти для разгрузки задач управления памятью. Это накладывает определенные накладные расходы, но вашему типичному бизнес-приложению на самом деле не нужно так много информации. Многие программы обходятся менее чем тысячей объектов. Ручное управление таким количеством - рутинная работа, но даже тысячи байтов на каждый объект не будут иметь значения.

В вашем случае накладные расходы на объект становятся проблемой. Вы можете, например, рассмотреть возможность представления каждого столбца как одного объекта, реализованного с одной строкой и массивом целочисленных смещений. Чтобы вернуть одно поле, вы возвращаете подстроку (возможно, в качестве прокладки)

...