Команда Go for Devs подготовила перевод статьи о том, как memory maps (mmap) обеспечивают молниеносный доступ к файлам в Go. Автор показывает, что замена обычного чтения и записи на работу с памятью может ускорить программу в 25 раз — и объясняет, почему это почти магия, но с нюансами.
Одно из самых медленных действий, которое можно совершить в приложении, — это системный вызов. Они медленные, потому что требуют перехода в ядро, а это дорогостоящая операция. Что делать, если нужно выполнять много операций ввода-вывода с диском, но при этом важна производительность? Один из вариантов — использовать memory maps.
Memory maps — это современный механизм Unix, позволяющий «включить» файл в виртуальную память. В контексте Unix «современный» означает появившийся где-то в 1980-х или позже. У вас есть файл с данными, вы вызываете mmap, и получаете указатель на участок памяти, где эти данные находятся. Теперь, вместо того чтобы делать seek и read, вы просто читаете по этому указателю, регулируя смещение, чтобы попасть в нужное место.
Производительность
Чтобы показать, какой прирост производительности можно получить с помощью memory maps, я написал небольшую библиотеку на Go, которая позволяет читать файл через memory map или через ReaderAt.ReaderAt использует системный вызов pread(), совмещающий seek и read, а mmap просто читает напрямую из памяти.
Случайное чтение (ReaderAt): 416.4 ns/op
Случайное чтение (mmap): 3.3 ns/op
---
Итерация (ReaderAt): 333.3 ns/op
Итерация (mmap): 1.3 ns/op
Почти как магия. Когда мы запускали Varnish Cache в 2006 году, именно эта возможность делала его настолько быстрым при выдаче контента. Varnish Cache использовал memory maps, чтобы доставлять данные с невероятной скоростью.
Кроме того, поскольку можно работать с указателями на память, выделенную memory map, снижается нагрузка на память и уменьшается общая задержка.
Обратная сторона memory maps
Недостаток memory maps в том, что по сути в них нельзя писать. Причина кроется в устройстве виртуальной памяти.
Когда вы пытаетесь записать в область виртуальной памяти, которая не отображена на физическую, процессор вызывает page fault. Современный CPU отслеживает, какие страницы виртуальной памяти соответствуют каким физическим страницам.
Если вы пишете в страницу, которая не сопоставлена, процессору нужна помощь.
При page fault операционная система:
Выделяет новую страницу памяти.
Считывает содержимое файла по нужному смещению.
Записывает это содержимое в новую страницу.
Затем управление возвращается приложению, и оно перезаписывает эту страницу новыми данными.
Можно просто поаплодировать тому, насколько это неэффективно. Думаю, можно с уверенностью сказать: писать через memory map — плохая идея, если важна производительность, особенно если есть риск, что файл ещё не загружен в физическую память.
Вот несколько бенчмарков:
Запись через mmap, страницы не в памяти: 1870 ns/op
Запись через mmap, страницы в памяти: 79 ns/op
WriterAt: 303 ns/op
Как видно, наличие страниц в кэше критически влияет на производительность. WriterAt, использующий системный вызов pwrite, даёт куда более предсказуемые результаты.
Тем не менее, в начале Varnish Cache действительно писал через memory map. Как-то это срабатывало — в основном потому, что конкуренты тогда были гораздо хуже.
Позже у Varnish Cache появился malloc backend, а у Varnish Enterprise — несколько Massive Storage Engines.malloc backend решал проблему, просто выделяя память через системный вызов malloc, а Massive Storage Engine использует io_uring, который настолько новый, что поддержка его пока ещё ограничена.
Использование memory maps для решения реальных проблем производительности
Последние пару недель я работал над файловой системой с поддержкой HTTP. Это часть нашего решения AI Storage Acceleration, предназначенного для высокопроизводительных вычислительных сред.
В этой файловой системе нам нужно было передавать данные каталогов по HTTP. Каталог — это, по сути, список файлов, символических ссылок и подкаталогов. Наивный подход — просто сериализовать всё в JSON, но JSON печально известен своей медлительностью.
Наша главная цель — производительность. Мы сделали набор бенчмарков, сравнив различные базы данных. CDB оказалась самой быстрой в целом.
Тем не менее, даже она тратила примерно 1200 наносекунд на запрос к БД, полностью находящейся в кэше страниц. Это казалось подозрительно медленным. Ведь всё в памяти — зачем 1200ns на чтение? Это должно быть хотя бы в сто раз быстрее.
Я посмотрел реализацию CDB, которую использовал. Там применялся тот же подход через ReaderAt, так что большая часть времени, вероятно, уходила на ожидание операционной системы.
Через несколько часов я заменил seek/read на использование memory map.
Результат — ускорение в 25 раз. И снова — почти магия. В отличие от старого файлового стевидора в Varnish Cache, здесь это ускорение не имеет никаких побочных эффектов.
Бенчмарки: https://github.com/perbu/mmaps-in-go
CDB64 файлы с memory maps: https://github.com/perbu/cdb
Русскоязычное Go сообщество

Друзья! Эту статью подготовила команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Комментарии (9)

AntonLarinLive
25.10.2025 12:59Функция
CreateFileMappingбыла в WinAPI как минимум в Windows NT 3.51, а это 1995 год. Похвально, что через 30 лет она появилась и в таком "новаторском" продукте как Go.
ilving
25.10.2025 12:59Так в пакете syscall этот метод тоже чуть не с рождения гошки. Да и в статье речь о том, что человек сделал обертку над сисколлами.

diderevyagin
25.10.2025 12:59Системный вызов mmap как часть POSIX появился в середине 1980-х годов. Смысл вспоминать про WinAPI нет - как технология есть давно.
Но mmap не серебряная пуля - есть и недостатки. При дефиците ОЗУ от нее больше вреда, чем пользы.
Однако сейчас, когда ОЗУ относительно недорогая, такие концепции обретают новую жизнь. А многие с ними знакомы слабо .... и очень правильно просвещать, смотрите а вот есть такое и такое, подумайте и если что - используйте пожалуйста.
aamonster
Действительно всё настолько плохо с производительностью "обычных" API или, поигравшись с ними (по сути – закэшировав данные и уменьшив количество операций ввода/вывода/, можно достичь той же скорости, что и с memory map (грубо говоря, проблема уровня "не надо читать каждый байт отдельно")?
Всегда считал, что memory maps в основном упрощают жизнь, позволяя избавиться от кучи потенциально содержащего ошибки кода, а не кардинально улучшают производительность.
BadNickname
Системные вызовы это дорого, а block cache и mmap у нас уже есть.
Можно переписать в свою программу жирный кусок ядра линукса, параллельно потеряв все плюсы системного управления памятью, но зачем?
aamonster
Вообще не призываю переписывать, просто мне странно видеть "использование mmaps ускорило наш код в 25 раз" вместо "использование mmaps позволило нам удалить кучу легаси-кода, теперь проект гораздо проще поддерживать".
BadNickname
Если ты не пишешь что-то что делает много мелких чтений с диска - у тебя в принципе нет необходимости это оптимизировать.
Если что-то начало делать много мелких чтений с диска и оптимизации дошли до этого чего-то - кажется логичным взять mmap и ускорить код в 25 раз, а не писать легаси код для кешей и всего связанного, чтобы потом заменять его на mmap
aamonster
Именно! Причём обычно это понятно ещё на этапе проектирования.
vmx
Линейное чтение больших файлов (большими кусками) - да, будет приблизительно одинаково. Если нужно возиться с одними и теми же данными в небольшой области - mmap-решение может быть проще и быстрее.
Но вообще кеширование есть в "обычных" fread/fwrite из libc, размер буфера можно потюнить, непонятно почему сравнивали только сисколы read/write с mmap().
Да, но с нюансами. read/write при ошибке чтения/записи возвращают эту ошибку, и обычно программист к этому готов. С mmap в таком же случае приложение может получить SIGBUS и упасть. Программист и пользователи обычно к этому не готовы.
Управлять памятью в случае с mmap тоже сложнее. C read/write вы выделяете буфер и с ним работаете, все более-менее предсказуемо. А если вы сделаете в виртуалке с 8Gb памяти mmap на файл 32Gb и начнете его читать в разных местах, то контролировать память становится довольно проблематично. Все будет делать ОС, и иногда довольно непредсказуемо.