Когда я ищу какую-то позицию в файле и записываю небольшой объем данных (20 байтов), что происходит за кулисами?
Мое понимание
Насколько мне известно, наименьшая единица данных, которую можно записать или прочитать с диска, - это один сектор (традиционно 512 байт, но этот стандарт теперь меняется). Это означает, что для записи 20 байтов мне нужно прочитать целый сектор, изменить часть его в памяти и записать его на диск.
Это то, что я ожидаю, что произойдет в небуферизованном вводе-выводе. Я также ожидаю, что буферизованный ввод-вывод будет делать примерно то же самое, но быть умным в его кеше. Поэтому я бы подумал, что если я удалю локальность из окна, выполняя случайные попытки и пишет, как буферизованный, так и небуферизованный ввод-вывод должен иметь схожую производительность... может быть, с небуферизованным выходом немного лучше.
И снова я знаю, что это безумие для буферизованного ввода-вывода только для буфера одного сектора, поэтому я также могу ожидать, что он будет работать ужасно.
Мое приложение
Я сохраняю значения, собранные драйвером устройства SCADA, который получает удаленную телеметрию на сумму более ста тысяч точек. В файле есть дополнительные данные, так что каждая запись составляет 40 байт, но во время обновления необходимо записать всего 20 байтов.
Тест для предварительной реализации
Чтобы проверить, что мне не нужно придумывать какое-то блестяще переработанное решение, я проверил тест, используя несколько миллионов случайных записей, записанных в файл, который может содержать в общей сложности 200 000 записей. Каждый тест сгенерирует генератор случайных чисел с тем же значением, чтобы быть справедливым. Сначала я стираю файл и складываю его на общую длину (около 7,6 мегабайт), затем цикл несколько миллионов раз, передавая случайное смещение файла и некоторые данные в одну из двух тестовых функций:
void WriteOldSchool( void *context, long offset, Data *data )
{
int fd = (int)context;
lseek( fd, offset, SEEK_SET );
write( fd, (void*)data, sizeof(Data) );
}
void WriteStandard( void *context, long offset, Data *data )
{
FILE *fp = (FILE*)context;
fseek( fp, offset, SEEK_SET );
fwrite( (void*)data, sizeof(Data), 1, fp );
fflush(fp);
}
Возможно, нет сюрпризов?
Метод OldSchool
вышел сверху - много. Это было в 6 раз быстрее (1,48 миллиона против 232000 записей в секунду). Чтобы убедиться, что я не запускал аппаратное кэширование, я расширил размер своей базы данных до 20 миллионов записей (размер файла 763 мегабайта) и получил те же результаты.
Прежде чем указать на очевидный вызов fflush
, позвольте мне сказать, что удаление его не повлияло. Я предполагаю, что это связано с тем, что кеш должен быть зафиксирован, когда я ищу достаточно далеко, и это то, что я делаю большую часть времени.
Итак, что происходит?
Мне кажется, что буферизованный ввод-вывод должен читать (и, возможно, записывать все) большой фрагмент файла всякий раз, когда я пытаюсь писать. Поскольку я почти никогда не использую его кеш, это очень расточительно.
Кроме того (и я не знаю деталей аппаратного кэширования на диске), если буферизованный ввод-вывод пытается написать кучу секторов, когда я меняю только один, что снизит эффективность кэша оборудования.
Есть ли эксперты по диску, которые могут комментировать и объяснять это лучше, чем мои экспериментальные выводы? =)