1. Вступ
Навіщо потрібна буферизація та порівняння швидкодії
Ми вже зʼясували, що буферизація — це стратегія, що дає змогу об’єднувати дані у великі «пакети», аби звертатися до диска не сотні тисяч разів, а значно рідше, передаючи одразу багато інформації. Це дає відчутний приріст продуктивності, особливо на великих обсягах.
Коли варто замислитися про продуктивність вводу-виводу
- Якщо ви працюєте з великими файлами (гігабайти, терабайти — скажімо, колекція журналів подій за всі роки роботи вашої компанії).
- Якщо потрібна мінімальна затримка (наприклад, обробка журналу подій у реальному часі).
- Якщо кількість звернень до файлів величезна (наприклад, масове перейменування чи копіювання фотоархіву).
- Щоб на співбесіді показати, що ви розумієте сучасні підходи до оптимізації (і надаєте перевагу вимірюванню швидкості, а не принципу «аби якось працювало»).
У .NET за замовчуванням більшість файлових потоків уже мають свою буферизацію, але інколи потрібне тонке налаштування або особлива стратегія.
Основні «гравці»: які буфери існують
| Клас | Буфер за замовчуванням | Чи можна змінювати розмір | Застосування |
|---|---|---|---|
|
Є (4 096 байтів) | Так (через конструктор) | Базовий файловий потік |
|
Так (4 096 байтів) | Так (через конструктор) | «Обгортка» над потоком |
|
Так (1 024/1 024 байтів) | Так (конструктор) | Робота з текстом |
- BufferedStream може «обгорнути» інший потік для підвищення продуктивності (наприклад, якщо базовий потік погано буферизується або потрібен більший буфер).
- Розмір буфера — це компроміс між швидкістю та споживанням оперативної памʼяті.
2. Приклади:
Порівняймо три підходи для копіювання великого файлу:
- Без буфера — по одному байту (шкідливо, але наочно!)
- Буфер за замовчуванням — стандартний FileStream з CopyTo
- Ручне керування буфером — передаємо буфер самі, оптимізуємо розмір
Для експериментів створимо просту утиліту для копіювання файлів, яку додамо у наш застосунок. Нехай файл називається 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 КБ до 4–8 МБ — більше майже не дає приросту.
- Не забувайте про оперативну памʼять, особливо якщо таких операцій або потоків багато.
Експериментуємо з розміром буфера
Спробуйте змінити розмір буфера в нашому прикладі (32 КБ, 128 КБ, 1 МБ, 4 МБ) і подивіться, де досягається пік продуктивності. Зазвичай «золота середина» — близько 1 МБ.
Сценарії, коли ручний буфер доречніший
- Коли потрібно контролювати обсяг використовуваної памʼяті (наприклад, програма запускається на слабкому сервері).
- Коли потік не буферизується автоматично (NetworkStream, власні потоки).
- Під час роботи з великою кількістю паралельних операцій — можна виділяти кожному потоку свій буфер оптимального розміру.
- Коли потрібно максимально пришвидшити обробку дуже великого файлу (наприклад, під час перетворення великого CSV-файлу).
4. Найкращі практики та типові помилки
Чи можна зробити буфер взагалі величезним? Мовляв, «памʼяті вдосталь». Можна, але навіщо? Занадто великий буфер інколи навіть знижує продуктивність: частина памʼяті простоює, а система може почати «гальмувати» через проблеми з кешуванням.
Ручна буферизація не пришвидшує все підряд. Для маленьких файлів або вже оптимізованих потоків (наприклад, FileStream з великим внутрішнім буфером) приріст буде мінімальним, а код — складнішим.
Типова пастка: забути закрити потік або не обробити виняток — файл залишиться заблокованим. Використовуйте using і обробку помилок (try-catch) під час роботи з файлами.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ