Перейти к основному содержимому
  1. Rust/

BufReader в Rust: ускоряем ввод-вывод

1861 слово·9 минут· loading · loading · ·
Rust Dev
about-rust - Эта статья часть цикла.
Часть 20: Эта статья

В этой статье разберём зачем нужен BufReader, как он работает и какие есть способы читать данные из файлов. Мы сравним разные подходы и посмотрим сколько они занимают времени.

🎙️ К статье прилагается подкаст-версия - лёгкий и образный рассказ с аналогиями. Включайте, если хотите услышать объяснение на слух.

Подкаст Rust в деталях. Выпуск 1

Трейты и чтение данных
#

В Rust трейты описывают общее поведение. Если тип реализует трейт - он “умеет” делать определённые действия.

Например, трейт Read говорит: “этот объект можно читать”. Под него попадают файлы (File), сетевые соединения (TcpStream), стандартный ввод (stdin).

Главный метод:

fn read(&mut self, buf: &mut [u8]) -> Result<usize>;

Он записывает доступные байты в буфер и возвращает число реально прочитанных. Чтобы загрузить все данные - .read() вызывают многократно.

Системные вызовы и их цена
#

Когда мы читаем файл или сокет через Read, каждый вызов .read() почти всегда приводит к системному вызову read(2) (на Linux/Unix) или его аналогу в других ОС.

⚙️ Разберём по шагам что же происходит когда в коде вызвается этот метод:

  1. Программа вызывает метод .read().
  2. Процессор переключается из пользовательского режима (user mode) в режим ядра (kernel mode).
  3. ОС выполняет реальную операцию чтения: обращается к диску, сети или памяти.
  4. Управление возвращается в пользовательское приложение.

Каждый такой переход - это сотни инструкций процессора. Поэтому чтение по 1 байту с диска будет катастрофически медленным.

BufReader
#

Чтобы уменьшить число системных вызовов существует BufReader.

Он один раз считывает крупный блок (по умолчанию 8 КБ) во внутренний буфер, а дальше выдаёт данные уже из памяти. Так вместо тысячи вызовов вы получаете десятки.

BufReader: чтение всего файла
Шаг 1/2
 1// ?hidden:start
 2use std::fs::{File, OpenOptions};
 3use std::io::{self, BufReader, Read, Write};
 4use std::path::Path;
 5
 6fn ensure_file() -> io::Result<()> {
 7    if !Path::new("hello.txt").exists() {
 8        let mut f = OpenOptions::new().create(true).write(true).open("hello.txt")?;
 9        writeln!(f, "Знакомство с BufReader!")?;
10    }
11    Ok(())
12}
13// ?hidden:end
14fn main() -> io::Result<()> {
15    // Проверяем, что файл существует (создаём при необходимости)
16    ensure_file()?;
17    
18    // Открываем файл
19    let file = File::open("hello.txt")?;
20    
21    // Оборачиваем его в BufReader
22    let mut reader = BufReader::new(file);
23
24    // Читаем содержимое в строку
25    let mut contents = String::new();
26    reader.read_to_string(&mut contents)?;
27
28    println!("Содержимое файла: {}", contents);
29
30    Ok(())
31}
32

Разберем подробнее:

  1. Функция ensure_file проверяет наличие файла hello.txt. Если его нет — создаём и записываем туда строку “Знакомство с BufReader!”.
  2. В main открываем файл и оборачиваем его в BufReader. Буферизованное чтение делает работу эффективной.
  3. С помощью read_to_string считываем всё содержимое в строку.
  4. Результат печатаем в консоль.

Это самый простой пример: мы просто читаем файл целиком. Но ключевая идея уже видна - BufReader используется как оболочка вокруг файла, которая делает операции ввода-вывода более эффективными.

BufRead против Read
#

BufReader реализует трейт BufRead, который расширяет возможности Read.

Зачем нужен отдельный трейт? Потому что некоторые операции становятся эффективными только при наличии внутреннего буфера.

Например, метод .lines() возвращает итератор по строкам:

BufReader: построчное чтение
 1use std::fs::{File, OpenOptions};
 2use std::io::{self, BufReader, BufRead, Write};
 3
 4// Создадим файл
 5fn ensure_file() -> io::Result<()> {
 6    if !std::path::Path::new("data.txt").exists() {
 7        let mut f = OpenOptions::new().create(true).write(true).open("data.txt")?;
 8        writeln!(f, "Первая строка")?;
 9        writeln!(f, "Вторая строка")?;
10        writeln!(f, "Третья строка")?;
11    }
12    Ok(())
13}
14
15fn main() -> io::Result<()> {
16    ensure_file()?;
17
18    let file = File::open("data.txt")?;
19    let reader = BufReader::new(file);
20
21    for line in reader.lines() {
22        println!("{}", line?);
23    }
24
25    Ok(())
26}
27

Разберем подробнее:

  1. BufReader читает файл большими блоками и хранит их в своём буфере.
  2. Трейт BufRead добавляет методы, которые работают не с файловым дескриптором напрямую, а с этим буфером в памяти.
  3. Метод .lines() ищет символ перевода строки \n в буфере и возвращает готовые строки.

Если бы у нас был только Read, то для поиска конца строки пришлось бы каждый раз вызывать .read() → это означало бы больше системных вызовов и ненужных копирований.

Другие полезные методы BufRead:
#

  • .read_line(&mut String) — читает одну строку целиком в переданную строку.
  • .split(u8) — возвращает итератор, разделяющий поток по заданному байту.
  • .fill_buf() — даёт прямой доступ к текущему содержимому буфера (без копирования).
  • .consume(n) — указывает сколько байт “съесть” из буфера после обработки.

Четыре способа читать файл построчно
#

Чтобы увидеть разницу, сравним четыре подхода.

Примеры для 4 подходов
Шаг 1/4
 1// ?hidden:start
 2use std::fs::{self, OpenOptions};
 3use std::io::{self, Write, Read};
 4use std::path::Path;
 5use std::fs::File;
 6
 7
 8/// Создаём файл big.txt, если его ещё нет.
 9/// 100_0 строк с текстом и номерами.
10fn ensure_file() -> io::Result<()> {
11    if Path::new("big.txt").exists() {
12        return Ok(());
13    }
14    println!("Создается файл big.txt...");
15    let mut f = OpenOptions::new().create(true).write(true).open("big.txt")?;
16    for i in 0..100_0 {
17        writeln!(f, "Строка номер {i:06} — пример содержимого файла для BufReader")?;
18    }
19    println!("Файл создан!");
20    Ok(())
21}
22// ?hidden:end
23fn main() -> io::Result<()> {
24    ensure_file()?; // создаём файл
25
26    let start = std::time::Instant::now();
27    let mut file = File::open("big.txt")?;
28    let mut buffer = [0; 1];
29    let mut line = String::new();
30
31    while file.read(&mut buffer)? > 0 {
32        let ch = buffer[0] as char;
33        if ch == '\n' {
34            line.clear();
35        } else {
36            line.push(ch);
37        }
38    }
39    let end = std::time::Instant::now();
40    println!("Время выполнения: {:?}", end - start);
41
42    Ok(())
43}
44
  1. Небуферизованное чтение по одному символу

❌ Очень медленно ~123 миллисекунды

Здесь каждая операция read идёт напрямую к ОС. Удобно для понимания, но не для реальной работы.

Когда BufReader не нужен
#

Хотя BufReader полезен в большинстве сценариев, есть ситуации, когда он не только не ускорит программу, но и будет лишним.

1. Данные уже в памяти
#

Если у вас есть Vec<u8>, String или срез &[u8], никакого дополнительного системного вызова не происходит. Данные уже находятся в оперативной памяти, а значит никакой выгоды от дополнительного буфера не будет.

2. Чтение крупными блоками
#

Если вы сразу читаете данные большими кусками (десятки или сотни килобайт), то накладные расходы на системные вызовы становятся несущественными.

3. Ограниченная память
#

Если у вас тысячи одновременных соединений (например, работаете с сервером), каждый BufReader создаёт внутренний буфер (по умолчанию 8 КБ). При тысячах соединений это может означать десятки мегабайт памяти, которые не всегда оправданы.

В асинхронном коде часто используют другие подходы: читают ровно столько - сколько пришло в сокете и сами управляют буферами чтобы экономить память.


Вывод
#

  • Read — низкоуровневый контракт для чтения потоков байтов.
  • Каждый вызов .read() - это системный вызов и он дорогой.
  • BufReader сокращает число системных вызовов и делает чтение в разы быстрее.

На практике BufReader с повторным использованием строки даёт лучший баланс между скоростью и памятью.

about-rust - Эта статья часть цикла.
Часть 20: Эта статья

Связанные статьи

Директивы компилятора в Rust
312 слов·2 минут· loading · loading
Rust Dev
Области видимости в Rust
342 слов·2 минут· loading · loading
Rust Dev
Разбираемся с ошибками в Rust
392 слов·2 минут· loading · loading
Rust Dev