Другое

Современный .NET: Лучшие практики преобразования потока в массив байтов

Узнайте предпочтительный подход для преобразования входных потоков в массивы байтов в .NET. Узнайте, почему MemoryStream.CopyTo() превосходит BinaryReader с помощью эталонных тестов производительности и примеров кода.

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

Вот мое текущее решение с .NET 3.5:

csharp
Stream s;
byte[] b;

using (BinaryReader br = new BinaryReader(s))
{
    b = br.ReadBytes((int)s.Length);
}

Все еще лучше считывать и записывать данные порциями из потока?

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

Предпочтительным методом создания байтового массива из входного потока в современном .NET является использование MemoryStream.CopyTo() с последующим вызовом ToArray(). Этот подход более эффективен и чище, чем использование BinaryReader, который вы сейчас используете. Этот метод является стандартом с версии .NET 4.0 и обеспечивает лучшую производительность и характеристики использования памяти.

Содержание

Современные лучшие практики .NET

В современном .NET (.NET 4.0 и новее) рекомендуется следующий подход:

csharp
public static byte[] StreamToByteArray(Stream inputStream)
{
    using (MemoryStream memoryStream = new MemoryStream())
    {
        inputStream.CopyTo(memoryStream);
        return memoryStream.ToArray();
    }
}

Этот подход использует встроенный метод CopyTo(), который был введен в .NET 4.0 специально для эффективного копирования из потока в поток. Согласно документации Microsoft, метод ToArray() “записывает содержимое потока в байтовый массив, независимо от свойства Position” и оптимизирован для производительности.

Сравнение производительности

Исследования показывают, что CopyTo() значительно эффективнее, чем использование BinaryReader:

  • Накладные расходы BinaryReader: Класс BinaryReader добавляет дополнительные уровни абстракции и был разработан в первую очередь для чтения примитивных типов данных, а не для сырых байтовых массивов. Как отмечено в исследованиях, “BinaryReader - это очень старый класс в .NET BCL, который не учитывает порядок байтов (endianness)”, что создает ненужные накладные расходы.

  • Эффективность копирования потоков: Как объясняет один из источников, “копирование из потока в поток обычно полезно в других ситуациях. Вам нужно написать всего один метод, и вы получаете копирование из потока в поток и копирование из потока в массив”.

  • Регрессия производительности: Есть свидетельства того, что производительность BinaryReader снизилась в более новых версиях .NET, один из источников отмечает: “Производительность улучшается примерно на 30%, если сначала прочитать весь файл в MemoryStream, а не использовать FileStream с BinaryReader”.

Анализ использования памяти

Шаблоны использования памяти значительно различаются между подходами:

  • Подход с MemoryStream: Использует память более эффективно, обычно выделяя только точное количество, необходимое для байтового массива. Как показывает один из анализов, при использовании CopyTo(), “это выделяет совершенно новый массив точного нужного размера и копирует данные”.

  • Подход с BinaryReader: Может использовать примерно вдвое больше памяти в некоторых сценариях, как упоминается в исследованиях: “Это означает, что в конечном итоге вы будете использовать примерно в 2 раза больше памяти при чтении всего файла”.

  • Управление буферами: Подход с MemoryStream обрабатывает управление буферами внутренне, в то время как BinaryReader требует дополнительного управления буферами, что может привести к неэффективности.


Работа с большими потоками

Для очень больших потоков (несколько сотен МБ или ГБ) вы все еще можете рассмотреть чтение по частям:

csharp
public static byte[] StreamToByteArrayLarge(Stream inputStream)
{
    const int bufferSize = 81920; // буфер 80KB
    using (MemoryStream memoryStream = new MemoryStream())
    {
        byte[] buffer = new byte[bufferSize];
        int bytesRead;
        while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            memoryStream.Write(buffer, 0, bytesRead);
        }
        return memoryStream.ToArray();
    }
}

Однако для большинства сценариев стандартный метод CopyTo() все равно предпочтительнее, потому что:

  1. Он реализован в оптимизированном нативном коде
  2. Он автоматически обрабатывает размер буфера
  3. Разница в производительности незначительна для большинства случаев использования

Полные примеры кода

Вот всестороннее сравнение разных подходов:

Современный .NET 4.0+ (Рекомендуемый)

csharp
// Подход с методом расширения
public static class StreamExtensions
{
    public static byte[] ToByteArray(this Stream stream)
    {
        using (var memoryStream = new MemoryStream())
        {
            stream.CopyTo(memoryStream);
            return memoryStream.ToArray();
        }
    }
}

// Использование:
byte[] bytes = inputStream.ToByteArray();

Устаревший .NET 3.5 (Ваш текущий подход)

csharp
// Ваша существующая реализация (работает, но менее эффективно)
public static byte[] StreamToByteArrayLegacy(Stream stream)
{
    using (BinaryReader br = new BinaryReader(stream))
    {
        return br.ReadBytes((int)stream.Length);
    }
}

Альтернативный подход с чтением по частям

csharp
public static byte[] StreamToByteArrayChunked(Stream stream)
{
    using (var memoryStream = new MemoryStream())
    {
        int bufferSize = 4096;
        byte[] buffer = new byte[bufferSize];
        int bytesRead;
        
        while ((bytesRead = stream.Read(buffer, 0, bufferSize)) > 0)
        {
            memoryStream.Write(buffer, 0, bytesRead);
        }
        
        return memoryStream.ToArray();
    }
}

Когда использовать альтернативные подходы

Хотя CopyTo() обычно предпочтительнее, существуют конкретные сценарии, где другие подходы могут быть лучше:

  1. Неизвестная длина потока: Если вы не можете заранее определить длину потока (хотя CopyTo() на самом деле этого не требует)

  2. Очень большие потоки с ограничениями памяти: Для extremely больших файлов, когда вы хотите обрабатывать данные по частям, а не загружать все в память

  3. Среды устаревшего .NET 3.5: Где CopyTo() недоступен (хотя обновление до .NET 4.0+ рекомендуется)

  4. Специализированные сценарии: Когда вам требуется дополнительная обработка во время операции чтения, которую предоставляет BinaryReader

Заключение

Предпочтительный метод преобразования потоков в байтовые массивы в современном .NET значительно эволюционировал с версии .NET 3.5. Основные выводы:

  1. Обновитесь до метода CopyTo(): Для .NET 4.0 и новее используйте stream.CopyTo(memoryStream) с последующим вызовом memoryStream.ToArray() для оптимальной производительности.

  2. Замените BinaryReader: Ваш текущий подход с BinaryReader, хотя и функционален, менее эффективен и использует больше памяти, чем современная альтернатива.

  3. Рассчитывайте чтение по частям только для очень больших файлов: Для большинства сценариев встроенный метод CopyTo() превосходит, но подходы с чтением по частям все еще могут быть полезны для extremely больших потоков.

  4. Используйте методы расширения: Создание метода расширения обеспечивает чистый, повторно используемый код, соответствующий современным соглашениям .NET.

Улучшения производительности и использования памяти в современном подходе делают его миграцию worthwhile away от шаблона BinaryReader, даже если ваш текущий код работает perfectly fine.

Источники

  1. How to Convert Stream to Byte Array in C# - Code Maze
  2. Creating a byte array from a stream - Stack Overflow
  3. How to Convert Stream to Byte Array in C# - Delft Stack
  4. MemoryStream.ToArray Method - Microsoft Learn
  5. Steam.CopyTo - Performance problems
  6. BinaryReader read performance regression - GitHub
  7. Handling reading large files in C# - Answer Overflow
Авторы
Проверено модерацией
Модерация