Как сортировать строки в большом(2Гб) файле?

102
30 августа 2019, 14:50

Строки разной длины и из разных символов, т.е. вариант с сортировкой подсчетом, не катит. Сделал внешнюю сортировку слиянием. Разбиваю файл на куски по 100Мб, сортирую каждый, выполняю слияние. Все это занимает у меня около 10 минут. Хотя рядом лежит чужой софт, который за 6 минут сортирует тот же объем и выполняет удаление дублей из него.

Прикладываю свой код.

public static class Sorting
{
    public static void ExternalMergeSort(string originalFile, string newFile)
    {
        //Разбить файл на части по 100 Мб
        string dir = SplitFile(originalFile);
        //Отсортировать каждую часть
        foreach (string file in Directory.GetFiles(dir, "*.txt", SearchOption.AllDirectories))
            InternalSort(file);
        //Многопоточный вариант - не хватает памяти
        //List<Task> tasks = new List<Task>();
        //foreach (string file in Directory.GetFiles(dir, "*.txt", SearchOption.AllDirectories))
        //{
        //    Task t = new Task(() => InternalSort(file));
        //    tasks.Add(t);
        //    t.Start();
        //}
        //Task.WaitAll(tasks.ToArray());
        //Объединить части
        MergeFilesInDirectory(dir);
    }

    /// <summary>
    /// Разбитие файла на куски укaзанного размера
    /// </summary>
    /// <param name="originalFile">Файл для разбития</param>
    /// <param name="maxFileSize">Максимальный размер части файла. Default = 100Mb</param>
    /// <returns>Путь к папке с частями файла</returns>
    private static string SplitFile(string originalFile, double maxFileSize= 1e+8)
    {
        TimeWatcher.Start("SplitFile");
        var lines = File.ReadLines(originalFile);
        string dir = Path.GetDirectoryName(originalFile);
        string extDir = dir + "/" + Path.GetFileNameWithoutExtension(originalFile);
        if (!Directory.Exists(extDir)) Directory.CreateDirectory(extDir);
        string partPath = extDir + "/" + Guid.NewGuid().ToString() + Path.GetExtension(originalFile);
        var outputFile = new StreamWriter( File.OpenWrite(partPath));
        foreach(string line in lines)
        {
            outputFile.WriteLine(line);
            if (outputFile.BaseStream.Position >= maxFileSize)
            {
                outputFile.Close();
                partPath = extDir + "/" + Guid.NewGuid().ToString() + Path.GetExtension(originalFile);
                outputFile = new StreamWriter(File.OpenWrite(partPath));
            }
        }
        TimeWatcher.Show("SplitFile", true);
        return extDir;
    }
    /// <summary>
    /// Внутренняя сортировка файла
    /// </summary>
    /// <param name="originalFile">Сортируемый файл</param>
    public static void InternalSort(string originalFile)
    {
        TimeWatcher.Start("InternalSort");
        List<string> list = File.ReadAllLines(originalFile).ToList();
        list.Sort();
        File.WriteAllLines(originalFile, list);
        TimeWatcher.Show("InternalSort", true);
    }
    /// <summary>
    /// Слияние файлов в указанной директории
    /// </summary>
    /// <param name="dir">директория с файлами</param>
    private static void MergeFilesInDirectory(string dir)
    {
        TimeWatcher.Start("MergeFilesInDirectory");
        // Открываем все файлы разом и формируем слой чтения
        List<StreamReader> readers = new List<StreamReader>();
        List<string> layer = new List<string>(readers.Count);
        foreach (string file in Directory.GetFiles(dir, "*.txt", SearchOption.AllDirectories))
        {
            var reader = new StreamReader(File.OpenRead(file));
            readers.Add(reader);
            layer.Add(reader.ReadLine());
        }
        //Создаем файл результата
        var writter = new StreamWriter(File.OpenWrite(dir + "/Result.txt"));
        int Id = 0;
        while(layer.FirstOrDefault(x=>x!=null) != null)
        {
            string min = layer.Min();
            Id = layer.IndexOf(min);
            layer[Id] = readers[Id].ReadLine();
            writter.WriteLine(min);
        }
        writter.Close();
        foreach (var reader in readers)
            reader.Close();
        foreach (string file in Directory.GetFiles(dir, "*.txt", SearchOption.AllDirectories))
        {
            if (Path.GetFileNameWithoutExtension(file) != "Result")
                File.Delete(file);
        }
        TimeWatcher.Show("MergeFilesInDirectory", true);
    }
}

Нашел возможность увеличить максимальный размер объекта(в .NET ограничение 2Гб). Благодаря этому можно спокойно прочитать весь файл в память, а уже там отрывать куски. Чтение 2 Гб(с SSD в DDR3) заняло 0.7 секунд.

<configuration>
  <runtime>
    <gcAllowVeryLargeObjects enabled="true" />
  </runtime>  
..

Подробности тут: https://social.msdn.microsoft.com/Forums/ru-RU/8d5880fe-108e-47d2-bbd7-4669e0aec1ec/-2-?forum=programminglanguageru

Answer 1

Малый файл читаем в память целиком как массив байт. Разбиваем массив на куски по разделителю 0x0A(разделитель включаем в строку). Сортируем получившийся массив кусков( Потребуется написать свой Comparer). Затем открываем BufferedStream на чтение большого файла и еще один такой же стрим на запись в выходной файл. Тут я для удобства использова StreamReader. Читаем строку. Получаем байты строки в unicode. И выполняем бинарный поиск этого массива байт строки в нашем отсортированном массиве кусков малого файла. Если не нашли - записываем в выходной файл. Так же можно дополнительно сохранять отсортированный массив в тот же файл, из которого его получили и каким то образом запоминать, что файл уже отсортирован и при следуещем его использовании не сортировать. Важно. Сборка должна выполняться под x64. Размер файла который читается целиком - не должен быть больше 2ГБ. Иначе функция File.ReadAllBytes выдаст исключение.

READ ALSO
Сортировка списка папок по вложенности

Сортировка списка папок по вложенности

Есть список директорий (просто строки), которые необходимо отсортироватьНапример: C:\Program Files\Microsoft C:\Program Files (x86) C:\Program Files

116
Перекодировка строк в читабельный вид

Перекодировка строк в читабельный вид

У меня есть данные в неизвестной мне кодировкеПредположительно (почти уверен) это windows-1251 или 1252

126
Конвертация DataColumn[] в схему MySQL

Конвертация DataColumn[] в схему MySQL

У меня есть массив DataColumn[] полученный из одной очень старой СУБДМне надо по этому массиву создать таблицу MySQL

123
Difference in effective length between states is too big. Transition preview will be disabled. UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)

Difference in effective length between states is too big. Transition preview will be disabled. UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)

Делаю 2D платформерСделал анимации, всё нарезал, сделал ветви в аниматоре и всё отлично работало, но после у меня случилась проблема и я в пользу...

408