1. Вступ
Перш ніж розпочинати використовувати новий клас, варто зрозуміти, навіщо він узагалі потрібен. Давайте розберімося, що відбувається, коли ви працюєте з файлом «напряму» за допомогою FileStream.
Коли ви викликаєте Read або Write для потоку, створеного через FileStream, насправді відбувається звернення до дискової підсистеми комп’ютера. Сам по собі цей процес (особливо на старих жорстких дисках, але й на нових SSD теж) значно повільніший, ніж робота з оперативною пам’яттю. Уявіть, що коли ви замовляєте у McDonaldʼs картоплю фрі, касир щоразу біжить по новий пакет на склад. Уявіть, якою довгою буде черга!
Якщо працювати з невеликими порціями даних, часті звернення до диска або мережі призводять до помітної втрати продуктивності. Чим більший обсяг даних — тим відчутніший ефект.
Коротка аналогія
Виходить, що потоки без буфера — це приблизно як їздити до магазину 10 разів, купуючи по одному йогурту за раз. Буферизований потік — це коли ви берете одразу цілий кошик йогуртів, зводячи кількість поїздок до мінімуму.
2. Клас BufferedStream: перший погляд
Для чого він потрібен
BufferedStream — це обгортка над будь-яким потоком (Stream), яка зберігає проміжний буфер у пам’яті. Коли ви записуєте дані, вони спочатку потрапляють у буфер, і лише коли буфер заповнюється — скидаються на диск однією великою операцією. Аналогічно з читанням: під час першого звернення завантажується відчутний фрагмент даних у пам’ять, а потім ці дані повертаються частинами з пам’яті, доки буфер не спорожніє.
Приклад коду: створення BufferedStream
Давайте створимо найпростіший приклад. Нехай маємо завдання записати 100 000 рядків у файл:
string filePath = "big_output.txt";
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var bufferedStream = new BufferedStream(fileStream);
using var writer = new StreamWriter(bufferedStream);
for (int i = 0; i < 100_000; i++)
{
writer.WriteLine($"Рядок номер {i}");
}
Console.WriteLine("Запис завершено!");
Коментар:
- Відкриваємо файл для запису через FileStream.
- Далі обгортаємо його в BufferedStream, а вже потім — у StreamWriter (він пише текстові рядки в потік).
- Щойно буфер заповнюється — дані скидаються на диск однією порцією.
3. Як працює буферизація «зсередини»
Розберімо це на схемі:
[Твій код] → [StreamWriter] → [BufferedStream] → [FileStream] → [Файл на диску]
Коли ви викликаєте метод WriteLine() у StreamWriter, текст спочатку записується у внутрішній буфер, потім через BufferedStream — ще в один буфер, а вже потім, коли буфер заповниться або потік буде закрито, дані скидаються на диск.
Скільки байтів у одному відрі?
Розмір буфера за замовчуванням — 4096 байт (4 КБ), але його можна вказати явно:
int myBufferSize = 16 * 1024; // 16 КБ
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...
Практична порада: На сучасних системах варто використовувати буфери 8–64 КБ. Для дуже великих файлових операцій — і більше. Але не захоплюйтеся: якщо ви працюєте на мікроконтролері зі 128 КБ оперативної пам’яті, то 64 КБ буфера — погана ідея.
4. Експеримент: порівняємо швидкість з буфером і без
Щоб зрозуміти, наскільки це важливо, напишімо тест, який порівнює запис даних через FileStream з буфером і без:
using System.Diagnostics;
using System.Text;
string data = new string('X', 1000); // 1 000 символів
void WriteWithoutBuffer()
{
using var fs = new FileStream("no_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: false);
for (int i = 0; i < 10_000; i++)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
fs.Write(bytes, 0, bytes.Length); // Прямо у файл — щоразу звернення до диска
}
}
void WriteWithBuffer()
{
using var fs = new FileStream("with_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None);
using var bs = new BufferedStream(fs, 16 * 1024);
for (int i = 0; i < 10_000; i++)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
bs.Write(bytes, 0, bytes.Length);
}
}
// Засікаємо час
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("Без буфера: " + sw.ElapsedMilliseconds + " мс");
sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("З буфером: " + sw.ElapsedMilliseconds + " мс");
Очікуваний вивід:
У більшості випадків із буфером ви побачите помітний приріст швидкості! Особливо, якщо у вас HDD. Якщо ж це SSD — ефект усе одно помітний, але не такий разючий.
5. Який буфер обрати? Порівняння і практика
У .NET є багато класів для буферизації. Спробуймо навести ясність:
| Клас | Для чого використовується | Буфер вбудований? | Потрібно використовувати BufferedStream? |
|---|---|---|---|
|
Робота з файлами | Так (4 КБ) | Майже не потрібно (але можна) |
|
Робота з мережею | Ні | Дуже бажано |
|
Читання/запис тексту | Так (від 1 КБ) | Зазвичай не потрібно |
|
Стиснення/розпакування | Ні | Можна/потрібно для прискорення |
Важливо:
FileStream з параметром конструктора bufferSize — по суті вже буферизований потік. Якщо ви самостійно вказали достатньо великий буфер, додатковий BufferedStream суттєвого виграшу не дасть. Але якщо ви користуєтеся іншим потоком (наприклад, мережевим), то BufferedStream — ваш помічник.
6. Приклад: копіювання файлу за допомогою BufferedStream
string source = "big_input.dat";
string dest = "big_output.dat";
int bufferSize = 64 * 1024; // 64 КБ
using var inputStream = new FileStream(source, FileMode.Open, FileAccess.Read);
using var outputStream = new FileStream(dest, FileMode.Create, FileAccess.Write);
using var bufferedInput = new BufferedStream(inputStream, bufferSize);
using var bufferedOutput = new BufferedStream(outputStream, bufferSize);
byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = bufferedInput.Read(buffer, 0, buffer.Length)) > 0)
{
bufferedOutput.Write(buffer, 0, bytesRead);
}
// Не забудьте Flush — інакше останні байти не потраплять на диск!
bufferedOutput.Flush();
Console.WriteLine("Копіювання завершено!");
Коментар:
- Читаємо дані великими блоками (64 КБ) з одного файлу через BufferedStream.
- Записуємо їх в інший файл, також використовуючи буфер.
- Після завершення циклу обов’язково викликаємо Flush() для запису останніх даних.
7. Корисні нюанси
Порада: коли справді потрібен BufferedStream
- Якщо ви працюєте з потоками, які не мають буферизації (наприклад, мережеві потоки або власні похідні від Stream);
- Якщо ви працюєте з великими обсягами бінарних даних (наприклад, копіювання файлів, перетворення форматів, резервне копіювання);
- Якщо ви оптимізуєте вже наявний код і бачите, що вузьким місцем є велика кількість дрібних операцій Write/Read.
Трохи про асинхронність і буферизацію
З появою асинхронних операцій (ReadAsync/WriteAsync) буферизація залишилася корисною, але варто пам’ятати: якщо ви використовуєте асинхронні методи поверх буфера, обробка все одно відбувається в межах пам’яті, а фізична взаємодія з диском ще сильніше мінімізується.
У .NET 8+ і .NET 9 буферизація вбудована ще глибше, і більшість класів уже за замовчуванням мають буфери. Але для сумісності з мережевими потоками або вашими власними реалізаціями все ще варто використовувати BufferedStream вручну.
Докладніше про асинхронність ви дізнаєтеся на рівні 58.
Візуальна схема роботи буферизованих потоків
flowchart LR
A[Ваш код] --> B[StreamReader/Writer]
B --> C[BufferedStream]
C --> D[FileStream]
D --> E[Файл/Пристрій]
- A — Ваш код, який викликає Write/Read.
- B — Потік високого рівня (працює з текстом або даними).
- C — Буферизація (групує дані для підвищення швидкості).
- D — Конкретна реалізація потоку (файл, мережа).
- E — Фізичний пристрій (жорсткий диск, SSD, мережа тощо).
Поради та трюки з практики
- Якщо ви пишете по одному рядку у файл (наприклад, логування), краще явно вказувати розмір буфера більший, ніж розмір одного рядка. Це дозволить швидше записувати великі порції даних.
- Якщо кожна дія має бути миттєво записана (наприклад, критичні логи), викликайте Flush() після кожного запису. Але це зменшує переваги буферизації!
- Якщо ви створюєте тимчасові файли, які після створення одразу ж видаляються, може виявитися несуттєвим, чи щось залишилося в буфері — але будьте обережні, якщо важливо, щоб файл гарантовано було записано.
- Працюючи з дуже великими файлами (наприклад, десятки гігабайт), не соромтеся підвищувати розмір буфера до 1_048_576 байт (1 МБ) і вище — головне, щоб вистачало оперативної пам’яті.
8. Типові помилки та нюанси використання
Якщо зараз у вас з’явився азарт і бажання «всюди додати буфер» — не поспішайте. Усе добре в міру!
Одна з частих помилок — забувати викликати Flush() або закривати потік, коли це потрібно. Якщо потік ще не закрито, а програма аварійно завершилася, останні байти можуть залишитися в буфері в оперативній пам’яті й не потрапити на диск. Наприклад, якщо ви пишете логування у файл і програма падає, останній запис може зникнути.
BufferedStream сам по собі не «бачить» кінця ваших логічних повідомлень — він просто чекає, доки накопичиться порція даних потрібного розміру. Тому для критично важливих речей (логування, резервні копії тощо) краще періодично примусово викликати Flush():
bufferedStream.Flush(); // Примушує буфер скинути дані на диск
Якщо ви користуєтеся StreamWriter, то в нього є власний буфер! Тобто за вкладеного використання буферизація відбувається двічі (і це не завжди добре). Часто достатньо одного рівня буфера, і якщо ви використовуєте StreamWriter, додатковий BufferedStream може бути не потрібен.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ