Я хотел создать быстрый сжатый диск для macOS, но научился сомневаться в собственных бенчмарках
Полгода назад я начал разрабатывать сжатую файловую систему для macOS. Замысел казался straightforward: использовать возможности системы, но сделать сжатие постоянным свойством диска, а не разовой операцией над отдельными файлами. В результате появился DenseDrive, однако главным итогом проекта стала не новая файловая система, а болезненный урок: даже убедительные измерения могут показывать совсем не то, что кажется на первый взгляд.
В macOS давно существует decmpfs - встроенный механизм прозрачного сжатия. Приложение продолжает работать с обычным файлом, хотя физически его содержимое хранится в упакованном виде. Проблема проявляется после изменений: если, например, сжать `node_modules`, а затем запустить сборку, часть файлов будет перезаписана и распакована. Через некоторое время каталог снова начнёт занимать почти прежний объём.
Мне хотелось получить иной сценарий: файл, расположенный на сжатом диске, должен оставаться сжатым после любого количества изменений. Иными словами, компрессия должна быть не отдельной процедурой, а фундаментальным свойством файловой системы. Появление в macOS 15.4 фреймворка FSKit сделало такой эксперимент практичным: файловую систему стало возможно реализовать в пользовательском пространстве, без собственного kernel extension.
Подробное описание архитектуры и результатов экспериментов можно найти в материале о разработке сжатого диска для macOS. Но наиболее интересная часть истории связана не с алгоритмом компрессии, а с тем, как легко ошибиться в интерпретации результатов.
KOIO: впечатляющее ускорение с неожиданными последствиями
У userspace-файловой системы есть неизбежные накладные расходы. Операция приложения проходит через ядро, затем передаётся процессу файловой системы, который возвращает ответ обратно. В FSKit для этого активно используется XPC. При чтении одного большого файла дополнительные переходы могут быть не слишком заметны. Однако каталог с 20 тысячами небольших файлов создаёт совсем другую нагрузку: lookup, получение атрибутов, их изменение, открытие и закрытие выполняются для каждого объекта.
Поэтому особый интерес вызвал Kernel-Offloaded I/O, или KOIO. В стандартном режиме цепочка выглядит примерно так: приложение → ядро → FSKit → процесс файловой системы → накопитель. При использовании KOIO файловая система сообщает ядру, в каких физических экстентах находятся данные, после чего ядро обращается к устройству напрямую.
На раннем блочном тесте обычный путь показал около 611 МБ/с, а KOIO - примерно 1445 МБ/с. Разница почти в 2,5 раза выглядела как окончательное подтверждение правильности архитектуры. Но именно здесь скрывалась одна из самых опасных ошибок проекта.
Структура `FSVolumeExtent` описывает физическое расположение данных. Документация предупреждает: ядро может продолжать использовать переданный экстент, пока существует соответствующий vnode. Это внутренний объект ядра, связанный с файлом или каталогом. Его жизненный цикл не обязан завершаться сразу после закрытия файла приложением.
В DenseDrive работало фоновое "дожатие": файловая система брала ранее записанные блоки, сжимала их и при необходимости переносила в другую область контейнера. Для внутренней логики старый экстент после этого становился свободным. Но ядро могло всё ещё считать его действительным и продолжать выполнять чтение по прежнему адресу. В результате скорость обернулась риском повреждения данных.
Это был первый важный вывод: оптимизация, ускоряющая путь чтения, одновременно расширяет область ответственности файловой системы. Нельзя освобождать или переиспользовать блок только потому, что пользовательский дескриптор уже закрыт. Необходимо учитывать состояние vnode и гарантировать, что ядро больше не обращается к старому размещению.
Почему бенчмарки начали вводить в заблуждение
После этого я несколько раз попадал в ловушку красивых цифр. Сначала казалось, что проблема связана с формулой расчёта или неточным таймером. На практике причиной часто оказывалась сама постановка эксперимента.
Один из тестов неожиданно демонстрировал квадратичную зависимость времени от количества файлов - случайный O(N²). В другом случае узким местом оказался слишком дорогой `statfs`, который вызывался значительно чаще, чем предполагалось. Ещё одна ошибка была связана с быстрым режимом: на каждый файл он расходовал по 256 КиБ служебного пространства. На небольших наборах данных такой подход выглядел эффективным, но при масштабировании резко увеличивал расход памяти и диска.
Особенно показательно, что наилучшее улучшение коэффициента сжатия оказалось вообще не связано с компрессором. Основной выигрыш дал пересмотр размещения метаданных и поведения файловой системы. Это хороший пример того, почему оптимизация диска macOS не должна сводиться к выбору более быстрого алгоритма упаковки.
Ситуацию дополнительно усложнили разные конфигурации сборки. Один Release-build практически уничтожил аргументы в пользу быстрого режима. На следующий день другой тест показал противоположный результат. Позже выяснилось, что различия возникли из-за сочетания кэшей, размера тестовых файлов, фоновых операций и способа подготовки данных.
Даже права доступа однажды выглядели виновниками проблем с Finder. Казалось, что файловый менеджер пропускает часть объектов из-за неверных разрешений. Однако причина оказалась в двух строках кода, из-за которых Finder не получал ожидаемые файлы. SDK при этом не лгал: он действительно возвращал корректные значения для конкретного вызова, но не гарантировал, что вся последовательность операций соответствует ожиданиям клиента.
Что показал проект
Разработка DenseDrive напомнила, что быстрая файловая система для macOS строится не только вокруг пропускной способности. Важны жизненный цикл vnode, корректная работа с экстентами, согласованность кэшей, стоимость межпроцессных вызовов и поведение на реальных наборах данных.
Для проверки пришлось отказаться от одного универсального теста. Нужны были отдельные сценарии для больших последовательных файлов, множества мелких объектов, повторной записи, фонового сжатия, случайного доступа и заполнения диска. Только сопоставление таких профилей позволяет понять, где действительно помогает оптимизация, а где результат создаётся особенностями тестовой среды.
Полезно также разделять бенчмарки производительности macOS на измерения холодного и прогретого кэша. Запуск после перезагрузки, повторный запуск, работа с уже сжатыми блоками и тестирование на чистом контейнере могут давать радикально разные цифры. Если не фиксировать эти условия, сравнительная таблица превращается скорее в иллюстрацию, чем в доказательство.
Наконец, не стоит автоматически считать штатные программы для сжатия диска macOS полноценной альтернативой файловой системе. Они удобны для архивирования или одноразовой экономии места, но не решают задачу постоянного сжатия изменяемых файлов. Однако собственная реализация требует гораздо большей осторожности: ошибка в управлении блоками способна стоить не нескольких процентов производительности, а пользовательских данных.
Главный результат проекта оказался неожиданным. Я начинал с желания создать быстрый сжатый диск для macOS, а закончил исследованием границ доверия к измерениям. Числа полезны только тогда, когда понятны условия их получения, скрытые расходы и реальные последствия оптимизации. Иногда самый важный прогресс - не новый рекорд скорости, а обнаружение причины, по которой предыдущий рекорд нельзя считать достоверным.

