C# Сравнение производительности: Asyn c против Non-Asyn c Операция ввода-вывода в текстовом файле через: Read \ WriteAllLines vs Read \ WriteAllLinesAsync - PullRequest
3 голосов
/ 07 апреля 2020

Пока я работал с относительно большими текстовыми файлами, я заметил нечто странное. Asyn c Чтение и запись на самом деле медленнее, чем не-asyn c чтение:

E, g, выполнение этого фиктивного кода:

 var res1 = File.WriteAllLinesAsync(string.Format(@"C:\Projects\DelMee\file{0}.txt", i), lines);
 var res2 = File.WriteAllLinesAsync(string.Format(@"C:\Projects\DelMee\file{0}_bck.txt", i), lines);

 await res1;
 await res2;

на самом деле намного медленнее, чем

  File.WriteAllLines(string.Format(@"C:\Projects\DelMee\file{0}.txt", i), lines);
  File.WriteAllLines(string.Format(@"C:\Projects\DelMee\file{0}_bck.txt", i), lines);

Теоретически первый подход должен быть более быстрым, потому что второе сочинение следует рассмотреть до завершения первого. Разница в производительности составляет около 100% для файлов размером 15 ~ 25 МБ (10 против 20 секунд).

Я заметил одинаковое поведение для ReadAllLines и ReadAllLinesAsyn c.

Обновление: 0 Основная идея состоит в том, чтобы все файлы обрабатывались после завершения функции Функции TestFileWriteXXX. Поэтому

Task.WhenAll(allTasks1); // Without await is not a valid option

Обновление: 1 Я добавил чтение и запись, используя потоки, и это показало улучшение на 50%. Вот полный пример:

Обновление: 2 Я обновил код, чтобы устранить накладные расходы на генерацию буфера

        const int MaxAttempts = 5;
        static void Main(string[] args)
        {
            TestFileWrite();
            TestFileWriteViaThread();
            TestFileWriteAsync();
            Console.ReadLine();
        }

        private static void TestFileWrite()
        {
            Clear();
            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();

            Console.WriteLine( "Begin TestFileWrite");

            for (int i = 0; i < MaxAttempts; ++i)
            {
                TestFileWriteInt(i);
            }

            TimeSpan ts = stopWatch.Elapsed;
            string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
            Console.WriteLine("TestFileWrite took: " + elapsedTime);
        }

        private static void TestFileWriteViaThread()
        {
            Clear();
            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();

            Console.WriteLine("Begin TestFileWriteViaThread");

            List<Thread> _threads = new List<Thread>();

            for (int i = 0; i < MaxAttempts; ++i)
            {
                var t = new Thread(TestFileWriteInt);
                t.Start(i);
                _threads.Add(t);
            }

            _threads.ForEach(T => T.Join());

            TimeSpan ts = stopWatch.Elapsed;
            string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
            Console.WriteLine("TestFileWriteViaThread took: " + elapsedTime);
        }

        private static void TestFileWriteInt(object oIndex)
        {
            int index = (int)oIndex;
            List<string> lines = GenerateLines(index);

            File.WriteAllLines(string.Format(@"C:\Projects\DelMee\file{0}.txt", index), lines);
            File.WriteAllLines(string.Format(@"F:\Projects\DelMee\file{0}_bck.txt", index), lines);

            var text = File.ReadAllLines(string.Format(@"C:\Projects\DelMee\file{0}.txt", index));
            var text1 = File.ReadAllLines(string.Format(@"C:\Projects\DelMee\file{0}.txt", index));

            //File.WriteAllLines(string.Format(@"C:\Projects\DelMee\file_test{0}.txt", index), text1);
        }

        private static  async void TestFileWriteAsync()
        {
            Clear();

            Console.WriteLine("Begin TestFileWriteAsync ");
            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();

            for (int i = 0; i < MaxAttempts; ++i)
            {
                List<string> lines = GenerateLines(i);
                var allTasks = new List<Task>();

                allTasks.Add(File.WriteAllLinesAsync(string.Format(@"C:\Projects\DelMee\file{0}.txt", i), lines));
                allTasks.Add(File.WriteAllLinesAsync(string.Format(@"F:\Projects\DelMee\file{0}_bck.txt", i), lines));

                await Task.WhenAll(allTasks);

                var allTasks1 = new List<Task<string[]>>();
                allTasks1.Add(File.ReadAllLinesAsync(string.Format(@"C:\Projects\DelMee\file{0}.txt", i)));
                allTasks1.Add(File.ReadAllLinesAsync(string.Format(@"C:\Projects\DelMee\file{0}.txt", i)));

                await Task.WhenAll(allTasks1);

//                await File.WriteAllLinesAsync(string.Format(@"C:\Projects\DelMee\file_test{0}.txt", i), allTasks1[0].Result);
            }

            stopWatch.Stop();

            TimeSpan ts = stopWatch.Elapsed;
            string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
            Console.WriteLine("TestFileWriteAsync took: " + elapsedTime);
        }

        private static void Clear()
        {
            for (int i = 0; i < 15; ++i)
            {
                System.IO.File.Delete(string.Format(@"C:\Projects\DelMee\file{0}.txt", i));
                System.IO.File.Delete(string.Format(@"F:\Projects\DelMee\file{0}_bck.txt", i));
            }
        }


        static string buffer = new string('a', 25 * 1024 * 1024);
        private static List<string> GenerateLines(int i)
        {
            return new List<string>() { buffer };
        }

И результаты:

TestFileWrite занимает: 00: 00: 03.50

TestFileWriteViaThread принимает: 00: 00: 01.63

TestFileWriteAsyn c принимает: 00: 00: 06.78

8 Код CPU / C и F - два разных SSD-накопителя 850 EVO на двух разных SATA.

Обновление: 3 - Заключение Похоже, File.WriteAllLinesAsyn c хорошо справляется со сценарием когда мы хотим грипп sh большой объем данных. Как указывалось в ответах ниже, лучше использовать FileStream напрямую. Но асинхронный c все же медленнее, чем последовательный доступ.

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

Ответы [ 2 ]

2 голосов
/ 07 апреля 2020

Я думаю, что это известная проблема. Если вы Google, вы увидите кучу похожих сообщений.

Например: https://github.com/dotnet/runtime/issues/23196

Если fast является требованием для одного ввода-вывода Для этой операции всегда следует использовать методы syn c IO, а также методы syn c.

Write*Async методы внутренне открывают поток файлов в режиме асинхронного ввода-вывода, что требует дополнительных затрат по сравнению с syn c IO.

https://docs.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o

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

Также асинхронные методы в FileStream и StreamWriter могут иметь проблемы с небольшим размером буфера. Размер буфера по умолчанию для записи в файловый поток составляет 4 КБ, что слишком мало по сравнению с размером файла (от 25 МБ до 50 МБ). Несмотря на то, что размер буфера работает нормально для методов syn c, он преувеличивает накладные расходы, связанные с методами asyn c.

Посмотрите эту строку , метод выдает поток каждый раз, когда буфер заполнен. Если вы записываете файл размером 25 МБ с буфером 4096 байт по умолчанию, это происходит 6400 раз.

Чтобы оптимизировать это, если весь файл находится в памяти, размер буфера можно установить равным размеру файла, чтобы уменьшить контекст переключение и синхронизация между каждой записью и flu sh.

Если вы откроете FileStream и StreamWriter с разными размерами буфера в своем коде и запустите тесты для Write и WriteAsync, вы будете увидеть разницу. Если размер буфера идентичен размеру файла, разница между методами syn c и asyn c очень мала.

Например:

// 4KB buffer sync stream
using (var stream = new FileStream(
    path, FileMode.Create, FileAccess.Write, FileShare.Read, 
    4096, FileOptions.SequentialScan))
{
    using (var writer = new StreamWriter(stream, Encoding.UTF8))
    {
        writer.Write(str25mb);
    }
}

// 25MB buffer sync stream
using (var stream = new FileStream(
    path, FileMode.Create, FileAccess.Write, FileShare.Read, 
    25 * 1024 * 1024, FileOptions.SequentialScan))
{
    using (var writer = new StreamWriter(stream, Encoding.UTF8))
    {
        writer.Write(str25mb);
    }
}

// 4KB buffer async stream
using (var stream = new FileStream(
    path,
    FileMode.Create, FileAccess.Write, FileShare.Read,
    4096, FileOptions.Asynchronous | FileOptions.SequentialScan))
using (var writer = new StreamWriter(stream, Encoding.UTF8))
{
    await writer.WriteAsync(str25mb);
}

// 25MB buffer async stream
using (var stream = new FileStream(
    path,
    FileMode.Create, FileAccess.Write, FileShare.Read,
    25 * 1024 * 1024, FileOptions.Asynchronous | FileOptions.SequentialScan))
using (var writer = new StreamWriter(stream, Encoding.UTF8))
{
    await writer.WriteAsync(str25mb);
}

Результат (я запускаю каждый тест 10 раз) был:

TestFileWriteWithLargeBuffer took: 00:00:00.9291647
TestFileWriteWithLargeBufferAsync took: 00:00:01.1950127
TestFileWrite took: 00:00:01.5251026
TestFileWriteAsync took: 00:00:03.6913877
1 голос
/ 07 апреля 2020

Я ошибся с первым ответом, поскольку не использовал ожидание Task.WhenAll при попытке удалить async из TestFileWriteAsync.

У меня исправлен тест , и он показывает, что File.Write*Async действительно медленнее.

Begin TestFileWriteAsync
TestFileWriteAsync took: 00:00:13.7128699
Begin TestFileWrite
TestFileWrite took: 00:00:01.5734895
Begin TestFileWriteViaThread
TestFileWriteViaThread took: 00:00:00.8322218

Пожалуйста, извините

PS У меня есть проверил исходный код методов Asyn c.

Похоже, File.WriteAllLinesAsync и File.WriteAllTextAsync использует тот же InternalWriteAllTextAsyn c, который копирует часть исходного буфера еще раз

buffer = ArrayPool<char>.Shared.Rent(DefaultBufferSize);
int count = contents.Length;
int index = 0;
while (index < count)
{
 int batchSize = Math.Min(DefaultBufferSize, count - index);
 contents.CopyTo(index, buffer, 0, batchSize);
#if MS_IO_REDIST
 await sw.WriteAsync(buffer, 0, batchSize).ConfigureAwait(false);
#else
 await sw.WriteAsync(new ReadOnlyMemory<char>(buffer, 0, batchSize), cancellationToken).ConfigureAwait(false);
#endif

contents.CopyTo(index, buffer, 0, batchSize); - это строка, которая копирует часть исходного буфера данных.

Вы можете попробовать с File.WriteAllBytesAsync, он принимает буфер данных «как есть» и не выполняет дополнительная операция копирования:

Begin TestFileWriteAsync
TestFileWriteAsync took: 00:00:00.7741439
Begin TestFileWrite
TestFileWrite took: 00:00:00.5772008
Begin TestFileWriteViaThread
TestFileWriteViaThread took: 00:00:00.4457552

WriteAllBytesAsyn c тестовый исходный код

...