JavaRush /Курси /C# SELF /Швидкодія та ручне керування буфером

Швидкодія та ручне керування буфером

C# SELF
Рівень 41 , Лекція 3
Відкрита

1. Вступ

Навіщо потрібна буферизація та порівняння швидкодії

Ми вже зʼясували, що буферизація — це стратегія, що дає змогу об’єднувати дані у великі «пакети», аби звертатися до диска не сотні тисяч разів, а значно рідше, передаючи одразу багато інформації. Це дає відчутний приріст продуктивності, особливо на великих обсягах.

Коли варто замислитися про продуктивність вводу-виводу

  • Якщо ви працюєте з великими файлами (гігабайти, терабайти — скажімо, колекція журналів подій за всі роки роботи вашої компанії).
  • Якщо потрібна мінімальна затримка (наприклад, обробка журналу подій у реальному часі).
  • Якщо кількість звернень до файлів величезна (наприклад, масове перейменування чи копіювання фотоархіву).
  • Щоб на співбесіді показати, що ви розумієте сучасні підходи до оптимізації (і надаєте перевагу вимірюванню швидкості, а не принципу «аби якось працювало»).

У .NET за замовчуванням більшість файлових потоків уже мають свою буферизацію, але інколи потрібне тонке налаштування або особлива стратегія.

Основні «гравці»: які буфери існують

Клас Буфер за замовчуванням Чи можна змінювати розмір Застосування
FileStream
Є (4 096 байтів) Так (через конструктор) Базовий файловий потік
BufferedStream
Так (4 096 байтів) Так (через конструктор) «Обгортка» над потоком
StreamReader/Writer
Так (1 024/1 024 байтів) Так (конструктор) Робота з текстом
  • BufferedStream може «обгорнути» інший потік для підвищення продуктивності (наприклад, якщо базовий потік погано буферизується або потрібен більший буфер).
  • Розмір буфера — це компроміс між швидкістю та споживанням оперативної памʼяті.

2. Приклади:

Порівняймо три підходи для копіювання великого файлу:

  1. Без буфера — по одному байту (шкідливо, але наочно!)
  2. Буфер за замовчуванням — стандартний FileStream з CopyTo
  3. Ручне керування буфером — передаємо буфер самі, оптимізуємо розмір

Для експериментів створимо просту утиліту для копіювання файлів, яку додамо у наш застосунок. Нехай файл називається BigFile.bin.

class FileCopyBenchmarks
{
    // Копіювання по одному байту (антиприклад — так робити не треба!)
    public static void CopyOneByte(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        int b;
        while ((b = input.ReadByte()) != -1)
        {
            output.WriteByte((byte)b);
        }
    }

    // Копіювання за допомогою стандартного буфера FileStream
    public static void CopyWithDefaultBuffer(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        input.CopyTo(output); // Використовує внутрішній буфер (зазвичай 81920 байтів)
    }

    // Копіювання з ручним контролем буфера
    public static void CopyWithCustomBuffer(string source, string dest, int bufferSize = 1024 * 1024)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        byte[] buffer = new byte[bufferSize];
        int bytesRead;
        while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
        {
            output.Write(buffer, 0, bytesRead);
        }
    }
}

Яка з функцій швидша? Давайте виміряємо час виконання.

Як правильно вимірювати продуктивність

У .NET для вимірювання часу найпростіше використовувати Stopwatch:

static void Measure(Action action, string description)
{
    var sw = Stopwatch.StartNew();
    action();
    sw.Stop();
    Console.WriteLine($"{description}: {sw.ElapsedMilliseconds} мс");
}

Тепер спробуймо скопіювати один і той самий файл різними способами:

string source = "BigFile.bin";
string dest1 = "copy1.bin";
string dest2 = "copy2.bin";
string dest3 = "copy3.bin";

// Заздалегідь створіть файл BigFile.bin (наприклад, на 100–500 МБ) або використайте будь-який великий файл.

Measure(() => FileCopyBenchmarks.CopyOneByte(source, dest1), "CopyOneByte (по 1 байту)");
Measure(() => FileCopyBenchmarks.CopyWithDefaultBuffer(source, dest2), "CopyWithDefaultBuffer (стандартний)");
Measure(() => FileCopyBenchmarks.CopyWithCustomBuffer(source, dest3, 1024 * 1024), "CopyWithCustomBuffer (1 МБ)");

Небезпечні моменти та підводні камені

  • Якщо запускати багато разів поспіль, кеш ОС може «розігріти» диск, і наступні вимірювання будуть швидшими — для реальної оцінки краще перезапускати програму й очищати кеш.
  • Якщо файл маленький (10–20 КБ), переваги буферизації будуть непомітні — що більший файл, то відчутніша різниця.
  • Якщо задати розмір буфера занадто великим (наприклад, 100 МБ), може різко зрости споживання памʼяті та постраждає решта системи.

Візуалізація результатів: таблиця

Метод Час (мс) для файлу 500 МБ
По одному байту 100 000+
Стандартний FileStream / CopyTo 1 000–5 000
Ручний буфер 1 МБ 700–1 200

Цифри приблизні, але тенденція зрозуміла: що більший буфер, то менше звернень до диска і вища швидкість.

3. Анатомія ручного керування буфером

Навіщо іноді все ж хочеться налаштовувати розмір буфера? Ось проста аналогія: ви переїжджаєте в нову квартиру. Можна носити речі по одній чашці, а можна одразу взяти величезну коробку. Але й у коробки є межа, інакше її не підняти!

Як влаштовано читання з ручним буфером

// Приклад ручного керування розміром буфера
int bufferSize = 1024 * 1024; // 1 МБ
byte[] buffer = new byte[bufferSize];
int read;
while ((read = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
    outputStream.Write(buffer, 0, read);
}
  • Метод Read намагається заповнити весь буфер, але може повернути менше, якщо файл закінчується.
  • Розмір буфера часто підбирають у діапазоні від 32 КБ до 48 МБ — більше майже не дає приросту.
  • Не забувайте про оперативну памʼять, особливо якщо таких операцій або потоків багато.

Експериментуємо з розміром буфера

Спробуйте змінити розмір буфера в нашому прикладі (32 КБ, 128 КБ, 1 МБ, 4 МБ) і подивіться, де досягається пік продуктивності. Зазвичай «золота середина» — близько 1 МБ.

Сценарії, коли ручний буфер доречніший

  • Коли потрібно контролювати обсяг використовуваної памʼяті (наприклад, програма запускається на слабкому сервері).
  • Коли потік не буферизується автоматично (NetworkStream, власні потоки).
  • Під час роботи з великою кількістю паралельних операцій — можна виділяти кожному потоку свій буфер оптимального розміру.
  • Коли потрібно максимально пришвидшити обробку дуже великого файлу (наприклад, під час перетворення великого CSV-файлу).

4. Найкращі практики та типові помилки

Чи можна зробити буфер взагалі величезним? Мовляв, «памʼяті вдосталь». Можна, але навіщо? Занадто великий буфер інколи навіть знижує продуктивність: частина памʼяті простоює, а система може почати «гальмувати» через проблеми з кешуванням.

Ручна буферизація не пришвидшує все підряд. Для маленьких файлів або вже оптимізованих потоків (наприклад, FileStream з великим внутрішнім буфером) приріст буде мінімальним, а код — складнішим.

Типова пастка: забути закрити потік або не обробити виняток — файл залишиться заблокованим. Використовуйте using і обробку помилок (try-catch) під час роботи з файлами.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ