C# ускорить запись в файл

Доброго времени суток!

У меня есть такое задание:

Разработайте ПО, которая будет разбивать числа от 1 до 1 000 000 000 на группы, при этом числа в каждой отдельно взятой группе не делятся друг на друга...

Эти группы надо записать в файл. В общем - все хорошо до 100 млн. Записывается быстро(ну, или я так думаю) - 20 сек на разделение на группы, 20 сек на запись, 800 Мб веса. Но если поставить хотя бы 200 млн - то все, ошибка переполнения памяти. Из того, что вычитал и понял - BufferedStream ускоряет процесс. Последний вариант к которому пришел:

private static void FullCalculate(long numN, string filePath)
    {
        string newFilepath = filePath.Insert(filePath.IndexOf('.'), "_inGroup");

        if (File.Exists(newFilepath))
        {
            File.Delete(newFilepath);
        }
        StringBuilder nums = new StringBuilder("");
        int count = 2;
        
        Stopwatch st = new Stopwatch();
        Stopwatch st2 = new Stopwatch();
        st.Start();
        **using (StreamWriter sw = new StreamWriter(new BufferedStream(File.OpenWrite(newFilepath), 2 * 1024 * 1024)))**
        {
            nums.Append($"Группа {count - 1}: {count - 1}.\r");//Не стал мучаться с выводом 1 группы
            // в цикле, потому что она статична - "Группа 1: 1."
            //nums.AppendLine();// Убрал AppendLine  в надежде, что это повлияет на процесс
            nums.Append($"Группа {count}: ");
            for (int i = 2; i < numN + 1; i++)
            {
                if (i == Math.Pow(2, count))
                {
                    nums.Append(".\r");
                    //nums.AppendLine();
                    nums.Append($"Группа {count}: ");
                    count++;
                }

                nums.Append(i + " ");
            }
            nums.Append('.');
            st.Stop();

            st2.Start();
            sw.Write(nums); //запись в файл разбитых по группам чисел
            st2.Stop();
        }
        st.Stop();

Я пробовал сначала вот так:

 using (StreamWriter sw = new StreamWriter(Filepath))

По скорости выполнения и ошибкам, так же как и в последнем варианте.

Вопрос, собственно: Можно ли что то еще изменить, чтобы, если не повысить скорость, то хотя бы избавиться от ошибок???

По поводу переполнения памяти примерно понимаю - нужно записать StringBuilder в файл и продолжить дальше разделять на группы. И тогда(по идее) все запишется. Это верное направление или есть еще какие-нибудь варианты?

P.S.: Условия такие, что массивы использовать нельзя, делать в одном цикле.

Само разбиение на группы должно иметь примерно такой вывод(N =8):

Группа 1: 1.

Группа 2: 2 3.

Группа 3: 4 5 6 7.

Логика - разбивать по степеням 2-ки(вдруг нужно)


Ответы (1 шт):

Автор решения: aepot

Если решать в лоб, то получится как-то так, но вы делаете слишком много лишних вычислений. И генерируете просто огромного размера строки в памяти, это медленно.

private static void FullCalculate(long numN, string filePath)
{
    string newFilepath = filePath.Insert(filePath.IndexOf('.'), "_inGroup");

    if (File.Exists(newFilepath))
    {
        File.Delete(newFilepath);
    }
    int count = 2;

    using (StreamWriter sw = new StreamWriter(new BufferedStream(File.OpenWrite(newFilepath), 2 * 1024 * 1024)))
    {
        sw.WriteLine($"Группа {count - 1}: {count - 1}.");//Не стал мучаться с выводом 1 группы
            
        sw.Write($"Группа {count}: ");
        for (int i = 2; i < numN + 1; i++)
        {
            if (i == Math.Pow(2, count))
            {
                sw.WriteLine(".");
                sw.Write($"Группа {count}: ");
                count++;
            }

            sw.Write(i + " ");
        }
        sw.WriteLine(".");
    }
}

Я бы решал как-то так.

private static void FullCalculate(long numN, string filePath)
{
    string newFilepath = filePath.Insert(filePath.IndexOf('.'), "_inGroup");
    using (StreamWriter sw = new StreamWriter(new FileStream(newFilepath, FileMode.Create, FileAccess.Write, FileShare.None, 1024 * 1024, false)))
    {
        int count = 1;
        long threshold = 1;

        for (long i = 1; i <= numN; i++)
        {
           if (i == threshold)
           {
               sw.Write($"Группа {count++}: ");
               threshold *= 2;
           }
           sw.Write(i.ToString());
           if (i == threshold - 1)
               sw.WriteLine(".");
           else
               sw.Write(" ");
        }
    }
}

И вот когда ясно, что память используется эффективно, уже можно начинать играться с буферизацией.

Кстати, FileStream имеет свой внутренний буфер. В дополнительном создании BufferedStream нет необходимости.

Еще можно распараллелить генерацию данных и запись на диск на 2 потока. Есть такой шаблон многопоточного проектирования - Producer/Consumer. Это когда один поток генерирует данные, а второй потребляет, и они друг друга не ждут. Самый простой представитель реализации этого шаблона - BlockingCollection

private static void FullCalculate(long numN, string filePath)
{
    string newFilepath = filePath.Insert(filePath.IndexOf('.'), "_inGroup");
    BlockingCollection<string> items = new BlockingCollection<string>();
    Task task = Task.Run(() => WriterLoop(newFilepath, items));
    int count = 1;
    long threshold = 1;

    for (long i = 1; i <= numN; i++)
    {
       if (i == threshold)
       {
           items.Add($"Группа {count++}: ");
           threshold *= 2;
       }
       items.Add(i.ToString());
       if (i == threshold - 1)
           items.Add($".{Environment.NewLine}");
       else
           items.Add(" ");
    }
    items.CompleteAdding();
    // здесь всё сгенерировано
    task.Wait();
    // здесь всё записано на диск
}

private static void WriterLoop(string path, BlockingCollection<string> items)
{
    using (StreamWriter sw = new StreamWriter(new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 1024 * 1024, false)))
    {
        foreach (string item in items.GetConsumingEnumerable())
            sw.Write(item);
    }
}
→ Ссылка