Чтение файлов в C++: iostream, C-API, mmap — что выбрать
Один и тот же файл на 1 ГБ читается в C++ пятью способами, и разница по скорости доходит до порядка. Разбираем, чем платишь за каждый вариант и когда какой оправдан.

Задача звучит просто: прочитать файл и что-то с ним сделать. Но в C++ у вас минимум пять способов это сделать — std::ifstream посимвольно, ifstream блоками, C-шный fread, отображение в память через mmap и высокоуровневый std::filesystem плюс чтение целиком в строку. Разница между худшим и лучшим вариантом на файле в 1 ГБ — не проценты, а порядок. Ниже — чем каждый подход платит за свою скорость и когда его брать.
Материал про C++17/C++20 и стандартную библиотеку. Всё, что касается mmap, — POSIX (Linux, macOS); под Windows аналог — CreateFileMapping, синтаксис другой.
Пять вариантов и что у них под капотом
Прежде чем выбирать, надо понять, где каждый теряет время. Основных источников замедления два: количество системных вызовов (переход в ядро дорогой) и лишние копирования буфера.
1. ifstream посимвольно
Самый наивный вариант — читать по одному символу в цикле while (in.get(c)). Каждый get() — вызов через виртуальную машину потоков с проверкой состояния. Даже с внутренней буферизацией streambuf это медленнее блочного чтения в разы из-за оверхеда на вызов. Использовать стоит только для крошечных файлов, где скорость не важна.
2. ifstream блоками через read()
#include <fstream>
#include <vector>
// C++17. Читаем файл кусками по 64 КБ в свой буфер.
std::ifstream in("data.bin", std::ios::binary);
if (!in) return 1;
std::vector<char> buffer(64 * 1024);
std::size_t total = 0;
while (in.read(buffer.data(), buffer.size()) || in.gcount() > 0) {
std::streamsize bytes = in.gcount(); // сколько реально прочитано
total += static_cast<std::size_t>(bytes);
// обработка buffer[0..bytes)
}Здесь один вызов read() тянет большой блок, число обращений к streambuf падает на порядки. Обратите внимание на условие цикла: read() возвращает false при достижении конца, но последний неполный блок надо забрать через gcount() — типичное место, где теряют хвост файла.
3. C-шный fread
#include <cstdio>
#include <vector>
std::FILE* f = std::fopen("data.bin", "rb");
if (!f) return 1;
std::vector<char> buffer(64 * 1024);
std::size_t bytes;
while ((bytes = std::fread(buffer.data(), 1, buffer.size(), f)) > 0) {
// обработка buffer[0..bytes)
}
std::fclose(f);По скорости fread обычно на уровне ifstream::read или чуть быстрее — меньше абстракций над FILE*. Платите потерей типобезопасности, ручным fclose (или обёрткой в умный указатель с кастомным делитером) и отсутствием исключений — ошибки проверяете сами.
4. Чтение всего файла в строку
#include <fstream>
#include <sstream>
#include <string>
std::ifstream in("data.txt", std::ios::binary);
std::ostringstream ss;
ss << in.rdbuf(); // весь streambuf одним махом
std::string content = ss.str();Приём ss << in.rdbuf() удобен и достаточно быстр, потому что копирует буфер без посимвольного обхода. Минус очевиден: файл целиком в памяти плюс промежуточная копия в ostringstream. Для конфига в пару килобайт — идеально, для лога в гигабайт — вы удвоите потребление памяти на момент вызова ss.str().
5. mmap — отображение в память
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// POSIX. Отображаем файл только для чтения.
int fd = open("data.bin", O_RDONLY);
if (fd == -1) return 1;
struct stat st;
if (fstat(fd, &st) == -1) { close(fd); return 1; }
std::size_t size = static_cast<std::size_t>(st.st_size);
void* addr = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) { close(fd); return 1; }
const char* data = static_cast<const char*>(addr);
// работаем с data[0..size) как с обычным массивом
munmap(addr, size);
close(fd);При mmap вы не читаете файл — вы даёте ядру адресное пространство, а страницы подгружаются по page fault при первом обращении. Копирования из page cache в ваш буфер нет вовсе. Отсюда выигрыш на больших файлах при случайном доступе. Но есть цена: обращение к неподгруженной странице — это page fault, а обрезание файла другим процессом во время работы с отображением даёт SIGBUS. И на 32-битной системе большой файл в адресное пространство просто не влезет.
Сравнение
| Подход | Скорость | Память | Когда брать |
|---|---|---|---|
| ifstream посимвольно | очень низкая | минимум | почти никогда |
| ifstream блоками | высокая | размер буфера | потоковая обработка, портируемость |
| fread | высокая | размер буфера | C-код, интеграция с C-библиотеками |
| всё в строку | высокая | 2× размер файла | мелкие файлы целиком |
| mmap | очень высокая | виртуальная | большие файлы, случайный доступ |
Быстрый способ читать файл — тот, при котором данные меньше всего раз копируются и реже всего пересекают границу ядра. Всё остальное — детали синтаксиса.
Когда что выбирать
- Файл целиком помещается в память и мелкий (конфиг, JSON до нескольких мегабайт) — читайте в строку через
rdbuf. Простота важнее микросекунд. - Обрабатываете поток и не держите всё сразу (парсинг лога, конвертация) —
ifstream::readблоками. Подберите буфер: 64–256 КБ обычно оптимальны, дальше прирост стремится к нулю. - Интегрируетесь с C-библиотекой или нужен максимум контроля —
fread. Но обернитеFILE*в RAII, иначе на первом жеreturnв середине функции получите утечку дескриптора. - Большой файл со случайным доступом (индекс, база, поиск по смещениям) —
mmap. Здесь он безоговорочно выигрывает: ОС сама кеширует страницы, а вы работаете с файлом как с массивом. - Линейный проход по огромному файлу один раз —
mmapтут не даёт преимущества над блочнымread, а page fault-ы могут даже проиграть. Беритеifstream::read.
Что ломается на практике
Несколько ошибок встречаются в каждом втором проекте.
- Забыли
std::ios::binary. На Windows без этого флага\r\nсхлопывается в\n, и размер прочитанного не совпадает с размером файла. На бинарных данных это порча. - Не проверили открытие.
ifstream in(path)не бросает исключение по умолчанию — молча создаёт поток в failed-состоянии. Проверяйтеif (!in)сразу. - Потеряли хвост при блочном чтении. Условие
while (in.read(...))без учётаgcount()отбрасывает последний неполный блок. - mmap на пустом файле. Вызов
mmapс длиной 0 — ошибка (EINVAL). Проверяйтеst.st_sizeдо отображения. - mmap и внешнее усечение. Если другой процесс укоротит файл, обращение к уже отображённой, но исчезнувшей странице даст
SIGBUSи падение. Для разделяемых файлов это реальный риск.
Как замерить у себя
Абсолютные цифры зависят от диска (NVMe против HDD), файловой системы, состояния page cache и объёма данных, поэтому приводить «X мегабайт в секунду» бессмысленно. Меряйте на своём железе:
- Возьмите файл, близкий по размеру к реальным данным, и прогоните каждый вариант несколько раз.
- Первый прогон отбросьте — он греет page cache, и разница между «холодным» и «горячим» чтением огромна. Чтобы мерить холодное чтение на Linux, сбрасывайте кеш между запусками (требует прав root).
- Замеряйте через
std::chrono::steady_clockвокруг блока чтения, а не всей программы. - Смотрите не только время, но и пиковую память — для варианта «всё в строку» и
mmapэто принципиально разные картины.
Начните с ifstream::read блоками как разумного дефолта: он портируем, безопасен и быстр. Переходите на mmap только когда профилировщик покажет, что упёрлись именно в чтение, и доступ у вас случайный. Инвалидация page cache и работа с файлами больше объёма RAM — отдельная тема, здесь её не разбираем.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: std::basic_ifstream — cppreference, mmap(2) — Linux manual page
Частые вопросы
А если у меня Windows — mmap же не работает?
Почему мой ifstream читает быстро, а посимвольный вариант в десятки раз медленнее — там же тоже буфер?
Какой размер буфера ставить для ifstream::read?
Не сработало: mmap возвращает MAP_FAILED. В чём дело?
Стоит ли использовать std::filesystem для чтения содержимого файла?
Как безопасно закрывать FILE* при fread, чтобы не было утечки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.