1. Введение
После разработки виртуальной машины и компилятора в рамках хобби прошел год и захотелось попробовать реализовать ёмкий по алгоритмам проект по системному программированию.
Каждый разработчик "кровавого" enterprise в своей работе использует СУБД (SQL/NoSQL) и меня всегда искренне интересовало как они устроены в самом сердце, на самом низком уровне. Почитав документацию и исходный код SQLite и MongoDB, про используемые в индексах и интерпретаторах запросов алгоритмы, осознал, что несмотря на широкую распространенность и некую привычность, системы управления базами данных (СУБД) - это сложные программные продукты, реализация которых не всем под силу. Отлично - как раз то, что мне надо. С мотивацией разобрались, перейдем к делу.
Итак, для начала хорошо бы сформулировать высокоуровневую спецификацию требований. Boson - это легкая, встраиваемая документоориентированная база данных на С/С++:
Простой и понятный API для работы с базой данных.
Быстрый поиск документов по ID на основе B+ Tree индексов.
Хранилище коллекций документов формата ключ/значение (JSON).
Поддержка LRU-cache для ускорения файловых операций ввода/вывода.
Хранение данных в одном файле, без временных файлов и настроек.
Кроссплатформенный "чистый" C/C++ и отсутствие внешних зависимостей.
Предварительная упрощенная архитектура будет поделена на четыре основных программных слоя, каждый из которых даёт очередной уровень абстракции для следующего уровня. Выглядеть это будет вот так:
Начну поэтапную реализацию проекта снизу вверх. Первое - кэширование ввода/вывода.
2. Кэширование файлового ввода/вывода (CachedFileIO)
Все мы знаем, что скорость I/O накопителя (HDD/SSD) в сотни раз медленнее, чем работа с оперативной памятью (RAM), а RAM в сотни раз медленнее I/O кэш вашего процессора (CPU). При этом достоверно известно, что практически все приложения в "реальном мире" имеют некую степень локальности обращения к данным.
В ряде исследований есть статистика, что кэш размером 10-15% от размера базы данных даёт в среднем более 95% попаданий в кэш (cache hits), а соотношение операций чтения/записи примерно соотносятся как 70%/30% Поэтому, для производительности кэширование (многоуровневая организация памяти) - это оправданная стратегия. Производительность CachedFileIO будем оценивать в сравнении со стандартными функциями STDIO (fread, fwrite) - прирост более 50% будем считать успехом.
Будем считать атомарным блоком чтения/записи при работе с физическим накопителем (storage device) и кэшем в оперативной памяти (memory cache) массив байт фиксированного размера (8Кб). Назовем его Страница (Page). Предполагается, что операции чтения/записи выровненные по границам физических секторов диска (HDD) или блокам (SSD) выполняются значительно быстрее. Аналогичная ситуация и с оперативной памятью. Кэшем (cache) будем считать загруженные в оперативную память, наиболее часто/недавно использовавшиеся страницы файла.
Итак, как неоднократно говорилось выше класс CachedFileIO нам необходим для кэширования файловых операций ввода/вывода. Основные требования к нему заключаются в следующем:
-
Хранить в кэше наиболее используемые страницы и удалять самые старые. То есть использовать алгоритм кэширования Least Recent Used (LRU), но таким образом, чтобы размер кэша не влиял на его производительность, он может иметь как сотни, так и сотни тысяч страниц, а именно, отвечать следующим требованиям:
Сложность поиска необходимой страницы в кэше - O(1).
Сложность вставки страницы в кэш - O(1).
Сложность удаления страницы из кэша - O(1).
При записи использовать стратегию Fetch-Before-Write (FBW). Если запись осуществляется в страницу, которая не была ранее загружена, нужно предварительно загрузить её в кэш. Это необходимо, чтобы при изменении части страницы, мы не затерли то, что было в остальной части.
Реализация должна быть максимально "cache-friendly" для CPU. Желательно использовать непрерывные обычные массивы, чтобы при попадании в кэш, она вероятнее всего уже была в одном из уровней кэша CPU (находилась рядом).
Выравнивать запросы операций ввода/вывода на границы файловой страницы (8192 байта). Операции чтения/записи в файл выровненные по границам страниц (сектора на HDD или блока на SSD) выполняются значительно быстрее.
Реализация должна быть 64-битной (не иметь ограничений на файлы более 4Гб).
Иметь очень простой API, инкапсулирующий детали реализации.
Примечание: на текущем этапе, для упрощения, мы осознанно не включаем в требования по блокировке файлов (file lock), так как без специфичных для ОС API этого не сделать. Механизмы для безопасного многопоточного использования (thread safe) также сделаем позже.
Как некогда говорил А. Привалов, на когда-то независимом канале РБК, "вещи не такие сложные, как кажутся на первый взгляд. И не такие простые как на второй...". Поэтому начнем со структур данных, которые необходимы для реализации стратегии кэша LRU/FBW.
Чтобы поиск, вставка, удаление из/в кэш по сложности была O(1), нам нужно объединить свойства таких структур данных как хэш-таблица и связанный список. Первый нам даёт нужную скорость поиска, а второй вставки и удаления. Если в Java есть стандартный шаблон LinkedHashMap, то в стандартной библиотеке C++ такого нет. И прекрасно - реализуем сами.
Логика работы алгоритма LRU Cache при чтении будет следующая:
Пользователь CachedFileIO делает вызов метода чтения данных (read, readPage). Мы считаем какие страницы файла затрагивает операция, пробегаемся по списку запрошенных страниц.
Для каждой запрошенной страницы проверяем, есть ли она в кэше - ищем номер файловой страницы в хэш-таблице. Если страница нашлась в хэш-таблице - это попадание кэш, перемещаем её в начало списка как наиболее "свежую", копируем запрошенные данные в буфер пользователя.
Если не находим номер страницы в хэш-таблице, то берем свободную страницу кэша. Если свободных страниц кэша нет, то выгружаем (если была изменена - помечена DIRTY) на диск наиболее "старую" (последнюю в списке) страницу в кэше, удаляем из списка и хэш-таблицы. В конце, загружаем запрошенную страницу с диска, вставляя её номер в начало списка (List) и добавляя ссылку на узел списка в хэш-таблицу. Помечаем страницу как CLEAN ("чистая"), обозначая, что в неё после загрузки с диска изменения не вносились.
Копируем запрошенный участок данных файла из кэша в пользовательский буфер.
Логика алгоритма при записи будет следующая (Fetch-Before-Write):
Пользователь CachedFileIO делает вызов метода записи данных (write, writePage). Мы считаем какие страницы файла затрагивает операция, пробегаемся по списку запрошенных страниц.
Для каждой изменяемой страницы проверяем, есть ли она в кэше - ищем номер файловой страницы в хэш-таблице. Если страница нашлась в хэш-таблице - это попадание кэш, перемещаем её в начало списка как наиболее "свежую", записываем пользовательские данные в страницы кэша. Помечаем их как DIRTY (измененные).
Если не находим номер страницы в кэше, аналогично алгоритму чтения выгружаем наиболее старую страницу, загружаем с диска изменяемую и потом записываем данные в кэш, помечая страницу как DIRTY.
Все страницы которые были загружены и в них не вносились изменения помечаются как CLEAN ("чистые"), а в те которые что-то записывали, помечаем как DIRTY ("грязные"). При удалении из кэша, все грязные страницы сохраняются на накопитель, остальные просто выгружаются из оперативной памяти. При сбрасывании (flush) на накопитель всего кэша, страницы сортируются в памяти в порядке возрастания номера страницы в файле, только после чего записываются. Предполагается, что последовательная запись выполняется быстрее.
Класс CachedFileIO будет иметь следующее объявление:
#pragma once
#define _ITERATOR_DEBUG_LEVEL 0 // MSVC debug performance fix
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <unordered_map>
namespace Boson {
//-------------------------------------------------------------------------
constexpr uint64_t PAGE_SIZE = 8192; // 8192 bytes page size
constexpr uint64_t MINIMAL_CACHE = 256 * 1024; // 256Kb minimal cache
constexpr uint64_t DEFAULT_CACHE = 1*1024*1024; // 1Mb default cache
constexpr uint64_t NOT_FOUND = -1; // "Not found" signature
//-------------------------------------------------------------------------
typedef enum { // Cache Page State
CLEAN = 0, // Page has not been changed
DIRTY = 1 // Cache page is rewritten
} PageState;
typedef struct {
uint8_t data[PAGE_SIZE];
} CachePageData;
class alignas(64) CachePage { // Align to CPU cache line
public:
uint64_t filePageNo; // Page number in file
PageState state; // Current page state
uint64_t availableDataLength; // Available amount of data
uint8_t* data; // Pointer to data (payload)
std::list<CachePage*>::iterator it; // Cache list node iterator
};
//-------------------------------------------------------------------------
typedef // Double linked list
std::list<CachePage*> // of cached pages pointers
CacheLinkedList;
typedef // Hashmap of cached pages
std::unordered_map<size_t, CachePage*> // File page No. -> CachePage*
CachedPagesMap;
//-------------------------------------------------------------------------
typedef enum { // CachedFileIO stats types
TOTAL_REQUESTS, // Total requests to cache
TOTAL_CACHE_MISSES, // Total number of cache misses
TOTAL_CACHE_HITS, // Total number of cache hits
TOTAL_BYTES_WRITTEN, // Total bytes written
TOTAL_BYTES_READ, // Total bytes read
TOTAL_WRITE_TIME_NS, // Total write time (ns)
TOTAL_READ_TIME_NS, // Total read time (ns)
CACHE_HITS_RATE, // Cache hits rate (0-100%)
CACHE_MISSES_RATE, // Cache misses rate (0-100%)
WRITE_THROUGHPUT, // Write throughput Mb/sec
READ_THROUGHPUT // Read throughput Mb/sec
} CachedFileStats;
//-------------------------------------------------------------------------
// Binary random access LRU cached file IO
//-------------------------------------------------------------------------
class CachedFileIO {
public:
CachedFileIO();
~CachedFileIO();
bool open(const char* path, size_t cache = DEFAULT_CACHE, bool readOnly = false);
bool close();
bool isOpen();
size_t read(size_t position, void* dataBuffer, size_t length);
size_t write(size_t position, const void* dataBuffer, size_t length);
size_t readPage(size_t pageNo, void* userPageBuffer);
size_t writePage(size_t pageNo, const void* userPageBuffer);
size_t flush();
void resetStats();
double getStats(CachedFileStats type);
size_t getFileSize();
size_t getCacheSize();
size_t setCacheSize(size_t cacheSize);
private:
void allocatePool(size_t pagesCount);
void releasePool();
CachePage* allocatePage();
CachePage* getFreeCachePage();
CachePage* searchPageInCache(size_t filePageNo);
CachePage* loadPageToCache(size_t filePageNo);
bool persistCachePage(CachePage* pageInfo);
bool clearCachePage(CachePage* pageInfo);
uint64_t maxPagesCount; // Maximum cache capacity (pages)
uint64_t pageCounter; // Allocated pages counter
uint64_t totalBytesRead; // Total bytes read
uint64_t totalBytesWritten; // Total bytes written
uint64_t totalReadDuration; // Time of read operations (ns)
uint64_t totalWriteDuration; // Time of write operations (ns)
uint64_t cacheRequests; // Cache requests counter
uint64_t cacheMisses; // Cache misses counter
std::FILE* fileHandler; // OS file handler
bool readOnly; // Read only flag
CachedPagesMap cacheMap; // Cached pages map
CacheLinkedList cacheList; // Cached pages double linked list
CachePage* cachePageInfoPool; // Cache pages info memory pool
CachePageData* cachePageDataPool; // Cache pages data memory pool
};
}
Реализацию класса можно посмотреть тут CachedFileIO.cpp.
3. Тестирование CachedFileIO
Так как у меня на компе стоит только SSD для тестирования также купил HDD подключаемый через USB, чтобы сравнивать на разных типах устройств. По настоящему я удивился, когда увидел что скорость их работы одинаковая (!). Но потом понял в чем дело.
Интересно, что тестирование может оказаться более сложным занятием чем сама разработка. Например, потому что в операционной системе эффективно работает кэш файловой системы, плюс операции ввода/вывода ОС выполняются не синхронно (не немедленно), а добавляются в очередь. Это означает, что на относительно небольших объемах ввода/вывода, может показаться, что ввод/вывод осуществляется быстрее, чем реальная пропускная способность устройств. Замерять время в лоб - так себе идея. Поэтому для бенчмарков нужны достаточно большие объемы данных, которые превышают и объем кэша, и объем очередей ввода/вывода операционных систем. Также ранее упоминалось, что:
В ряде исследований есть статистика, что кэш размером 10-15% от размера базы данных даёт в среднем более 95% попаданий в кэш (cache hits), а соотношение операций чтения/записи примерно соотносятся как 70%/30%
Для тестирования предположим, что запросы данных будут нормально распределены: σ =4% от размера базы и локализованы в каком то одном месте - для CachedFileIO без разницы даже если в нескольких, лишь бы локализованы. Примерно вот так:
Далее методом Box Muller`а будем генерировать нормально распределенные псевдослучайны числа, чтобы запрашивать разные участки файла (но нормально распределенные):
/**
*
* @brief Box Muller Method
* @return normal distributed random number
*
*/
double CachedFileIOTest::randNormal(double mean, double stddev)
{
static double n2 = 0.0;
static int n2_cached = 0;
if (!n2_cached) {
double x, y, r;
do {
x = 2.0 * rand() / RAND_MAX - 1.0;
y = 2.0 * rand() / RAND_MAX - 1.0;
r = x * x + y * y;
} while (r == 0.0 || r > 1.0);
double d = sqrt(-2.0 * log(r) / r);
double n1 = x * d;
n2 = y * d;
double result = n1 * stddev + mean;
n2_cached = 1;
return result;
}
else {
n2_cached = 0;
return n2 * stddev + mean;
}
}
Будем делать тестирование с миллионом итераций для каждого теста, мелкими порциями данных сравнивая результаты производительности работы CachedFileIO (read, write, readPage, writePage) и STDIO (fread, fwrite). Вот тут сам бенчмарк на коленках без использования каких либо фреймворков. И вот что получилось:
В release сборке сравнение производительности CachedFileIO и STDIO при многократных запусках с разными параметрами дало следующие результаты:
50%-97% попадания в кэш даёт от 50%-600% прироста производительности.
35%-49% попадания в кэш даёт от 12%-36% прироста производительности
1%-25% попаданий в кэш дают падение производительности от 5%-20%
Значит всё сработало как надо или, точнее, как ожидалось.
Наблюдение: почему-то в компиляторе MSVC в режиме debug очень "тормозят" итераторы и мешают измерениям. В 20-30 раз в сравнении с release. Приходилось отключать их отладку _ITERATOR_DEBUG_LEVEL.
Итак, конечно можно еще много что сделать в CachedFileIO, но на текущий момент его возможностей достаточно, чтобы переходить к разработке следующего слоя базы данных Boson - StorageIO.
До встречи!
Комментарии (41)
e_u_g
01.01.2023 11:12+4почему-то в компиляторе MSVC в режиме debug очень "тормозят" итераторы
Непроверенная догадка: может компилятор добавляет в сгенерированный код дополнительные проверки, чтобы ловить всякие внештатные ситуации и облегчать отладку?
Basheyev Автор
01.01.2023 11:56Поэтому "тормозят" в кавычках. Да, отладка итераторов вещь полезная, но в моем проекте ненужная.
slonopotamus
01.01.2023 14:14-1Зачем гадать, если можно просто взять и посмотреть в исходники?
Basheyev Автор
01.01.2023 14:46+6Тут нет гаданий. По исходникам и нашел же, что bounds checking создают overhead. Так как я их проверяю сам, поэтому они мне не нужны.
tzlom
01.01.2023 12:10+3А если маппить на память, будет быстрее или нет?
И вы нигде лицензию не указали, возможно это стоит исправить.
Basheyev Автор
01.01.2023 12:15+1Да, спасибо про лицензию. Да, однозначно mmap и/или флаг O_DIRECT в open могут помочь, но это все же вызовы OS, а не C++. Поэтому на текущем этапе избегал (это часть требований).
tzlom
01.01.2023 12:39+2Ну как бы да, а с другой стороны вы уже используете _fseeki64 так что код все равно прибит к одной ОС :)
На самом деле мне не очевидно даст ли mmap какой-либо выигрыш кроме по умолчанию не блокирующей выгрузки на диск, которую в БД все равно приходится избегать.
vassabi
01.01.2023 13:09ну, _fseeki64 можно будет заменить на обычный, это гораздо менее ОС-зависимая функция (базовые открыть, закрыть, считать, записать файл сидят в либс https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#Portable-Positioning )
ostinru
01.01.2023 22:58+3Вот тут чуваки в соавторстве с Andy Pablo говорят что MMAP для баз данных - плохая идея:
Are You Sure You Want to Use MMAP in Your Database Management System? https://www.cidrdb.org/cidr2022/papers/p13-crotty.pdfПоэтому, только если IO_DIRECT
slonopotamus
01.01.2023 14:11скорость I/O накопителя (HDD/SSD) в сотни раз медленнее, чем работа с оперативной памятью (RAM)
А вот и нет. Скорость линейного чтения из DDR4 порядка 20-25GB/s. При этом вполне себе доступны NVMe SSD со скоростью чтения 6-7GB/s.
tupenbor
01.01.2023 14:34+4"DDR4 порядка 20-25GB/s"
С одного канала, одной планки. Обычно каналов, планок, в количестве от 2 до 8 и больше, соответственно и скорость в 2-8 раза больше. И если сравнивать с каким медленным SSD/SATA, то разница вполне может на сотню потянуть ;)slonopotamus
01.01.2023 14:47одной планки
Ну так и SSD можно несколько штук поставить.
tzlom
01.01.2023 16:17И завести в raid, иначе скорость не вырастет.
borovinskiy
01.01.2023 20:04А 7 ГБ/c с SSD в типичных нагрузках вы и сейчас не увидите. Увидите 70-100Мб/c в однопоток. Ну а ваши базы данных в однопоток и пишут и писать в 50 потоков, чтобы нагрузить SSD параллельной записью, просто не способны.
blind_oracle
01.01.2023 23:15+2SSD одним потоком последовательно вполне пишут свои 7ГБ\с. Вот со случайным доступом уже всё несколько грустнее.
borovinskiy
02.01.2023 00:24+1Ссылку на бенчмарк можно c 7ГБ в один поток последовательной записи?
cdriper
03.01.2023 10:49да пожалуйста
общедоступная информация, которая находится за минуту
borovinskiy
03.01.2023 17:084k 1Q 1T: 106 Мб чтение и 365 Мб запись. И это на файле в 1ГБ, то есть не пробивая SLC-кеш на запись, а как пробьете, скорость на запись упадет в разы.
Если же говорить про 1M Q8 T1, то это фактически пишут пачками по 8 Мб и в SSD действует внутренний распараллеливающий механизм, то есть это для ОС запись последовательная, а по факту она параллельная и на файле в 1ГБ это вообще тест ОЗУ SSD. А надо писать так, чтобы если уж такие объемы записи, чтобы пробить SLC кеш диска и тогда у вас скорость тоже драматическим образом упадет.
Поэтому дайте нормальный бенчмарк на файле с 200 Гб или такого размера, чтобы пробить SLC-кеш конкретного SSD (и уж тем более его ОЗУ). Лучше всего fio.edo1h
04.01.2023 05:50и на файле в 1ГБ это вообще тест ОЗУ SSD
AFAIK нет, RAM SSD используется в первую очередь для хранения таблицы транслятора, что же до кэширования записи, оно происходит только в рамках формирования целых страниц (NAND просто не умеет писать за раз меньше, чем страницу, которая сегодня значительно больше сектора, которым оперирует ОС).
Поэтому дайте нормальный бенчмарк на файле с 200 Гб или такого размера, чтобы пробить SLC-кеш конкретного SSD (и уж тем более его ОЗУ).
держите. не 7, конечно, но всё равно немалоroot@debian:~# fio -ioengine=libaio -sync=0 -direct=1 -name=test -bs=64k -iodepth=1 -rw=write -runtime=300 -filename=/dev/nvme3n1 -numjobs=1 test: (g=0): rw=write, bs=(R) 64.0KiB-64.0KiB, (W) 64.0KiB-64.0KiB, (T) 64.0KiB-64.0KiB, ioengine=libaio, iodepth=1 fio-3.25 Starting 1 process Jobs: 1 (f=1): [W(1)][100.0%][w=2216MiB/s][w=35.5k IOPS][eta 00m:00s] test: (groupid=0, jobs=1): err= 0: pid=665472: Wed Jan 4 02:45:51 2023 write: IOPS=35.4k, BW=2213MiB/s (2321MB/s)(648GiB/300001msec); 0 zone resets slat (nsec): min=2359, max=53196, avg=2517.88, stdev=116.96 clat (nsec): min=430, max=2151.0k, avg=25371.04, stdev=5988.31 lat (usec): min=24, max=2153, avg=27.92, stdev= 5.99 clat percentiles (usec): | 1.00th=[ 25], 5.00th=[ 25], 10.00th=[ 25], 20.00th=[ 25], | 30.00th=[ 25], 40.00th=[ 25], 50.00th=[ 25], 60.00th=[ 26], | 70.00th=[ 26], 80.00th=[ 27], 90.00th=[ 27], 95.00th=[ 28], | 99.00th=[ 29], 99.50th=[ 29], 99.90th=[ 36], 99.95th=[ 36], | 99.99th=[ 108] bw ( MiB/s): min= 2179, max= 2227, per=100.00%, avg=2214.44, stdev= 9.85, samples=599 iops : min=34866, max=35644, avg=35430.98, stdev=157.65, samples=599 lat (nsec) : 500=0.01% lat (usec) : 20=0.01%, 50=99.97%, 100=0.02%, 250=0.01%, 500=0.01% lat (usec) : 750=0.01%, 1000=0.01% lat (msec) : 2=0.01%, 4=0.01% cpu : usr=6.14%, sys=9.53%, ctx=10624294, majf=0, minf=14 IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0% submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0% complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0% issued rwts: total=0,10624239,0,0 short=0,0,0,0 dropped=0,0,0,0 latency : target=0, window=0, percentile=100.00%, depth=1 Run status group 0 (all jobs): WRITE: bw=2213MiB/s (2321MB/s), 2213MiB/s-2213MiB/s (2321MB/s-2321MB/s), io=648GiB (696GB), run=300001-300001msec Disk stats (read/write): nvme3n1: ios=46/10617848, merge=0/0, ticks=2/273002, in_queue=273004, util=100.00%
pm9a3
на блоках 4К ожидаемо будет меньше (≈350), на блоках 1М — больше (≈3500).
и дело тут не только в распараллеливании записи со стороны накопителя, а и в накладных расходах на сисколы и т. п.
PrinceKorwin
02.01.2023 07:56Ну почему же. Данные могут писать в несколько потоков. А вот wal-журнал, да. В один поток.
borovinskiy
02.01.2023 09:55Но все равно раз запись двойная, быстрее чем в один поток в wal не будет.
Да еще и частые fsync-и.edo1h
03.01.2023 12:44Да еще и частые fsync-и
не совсем в тему, но БД, всё-таки, это обычно datacenter ssd, на которых штраф за fsync околонулевой.
borovinskiy
03.01.2023 18:05+1Сейчас нет данных под рукой, но по памяти штраф не нулевой даже у Optane. А есть бенч где нулевой?
tupenbor
01.01.2023 17:16А как вы думаете, как часто устанавливают дисковый raid при одной планке памяти. И ниже уже написали про latency, которая намного отличается.
edo1h
01.01.2023 14:42+5только префетч не всегда можно устроить, а latency отличается на три порядка (50-100 нс против 50+ мкс)
Bober4eg
01.01.2023 18:40+2Шикарная работа. Очень шикарная.
А насчёт интерпретатора - не готов ответить на данный момент. Железо... Ограничения... Бюджеты...
WASD1
01.01.2023 20:59+1Отличная работа.
А учитывая, что вы это делаете ради самообразования \ любви к искусству (программирования) - снимаю шляпу.
ПС
А транзакции планируются?
Мне всегда казалось, что DBMS это что-то про гарантии консистентности данных (ну то есть по-простому начиная с транзакций).
Basheyev Автор
02.01.2023 03:51Спасибо! ACID - хотелось бы реализовать, но пока не разобрался как это делается в NoSQL.
Kotofay
01.01.2023 21:29+3А если вместо хэшмапы использовать битовые поля?
Для поиска бит можно использовать либо интринсики либо быстрые алгоритмы, например такой:
Индекс бита это номер страницы.
Индекс * размер страницы => смещение в mmap файле.// возвращает размер _bits в 32 битных беззнаковых словах. int size( bool units = true ); // получить индекс бита в 1 int getMSB() { int r = -1; #ifndef MACHINE_INSTRUCTIONS const char lt[ 256 ] = { #define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n - 1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, LT( 4 ), LT( 5 ), LT( 5 ), LT( 6 ), LT( 6 ), LT( 6 ), LT( 6 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ) }; register quint32 t, tt, v; for( int i = size( true ) - 1; i >= 0; i-- ) { v = _bits[ i ]; if( v ) { if( tt = v >> 16 ) { r = ( t = tt >> 8 ) ? 24 + lt[ t ] : 16 + lt[ tt ]; } else { r = ( t = v >> 8 ) ? 8 + lt[ t ] : lt[ v ]; } return ( i * 32 + r ); } } #else register unsigned long index; register quint32 v; for( int i = size( true ) - 1; i >= 0; i-- ) { v = _bits[ i ]; if( v ) { if( _BitScanReverse( &index, v ) ) { return i * ( sizeof( quint32 ) * 8U ) + index; } } } #endif return r; } // количество установленных бит int count() { register quint32 v = 0, c = 0; register int r = 0, i = 0, sizeInWords = size( true ); #ifndef MACHINE_INSTRUCTIONS for( ; i < sizeInWords; i++ ) { v = _bits[ i ]; v = v - ( ( v >> 1 ) & 0x55555555 ); v = ( v & 0x33333333 ) + ( ( v >> 2 ) & 0x33333333 ); c = ( ( v + ( v >> 4 ) & 0xF0F0F0F ) * 0x1010101 ) >> 24; r += c; } #else Q_UNUSED( v ); Q_UNUSED( c ); for( ; i < sizeInWords; i++ ) { r += __popcnt( _bits[ i ] ); } #endif return r; } // set bit // index >= 0 && < sizeInBits -- set bit by index // otherwise -- set all bits void set( int index = -1 ) { if( index >= 0 ) { if( index < size() ) _bits[ index / ( sizeof( quint32 ) * 8U ) ] |= ( 1U << ( index % ( sizeof( quint32 ) * 8U ) ) ); } else { _bits.fill( ~( 0U ) ); } } // clear bit // index >= 0 && < sizeInBits -- clear bit by index // otherwise -- clear all bits void clear( int index = -1 ) { if( index >= 0 ) { if( index < size() ) { quint32 bit = ( 1U << ( index % ( sizeof( quint32 ) * 8U ) ) ); int chunk = index / ( sizeof( quint32 ) * 8U ); _bits[ chunk ] |= bit; _bits[ chunk ] ^= bit; } } else { _bits.fill( 0 ); } } //index >= 0 && < sizeInBits -- is bit set by index? // otherwise -- is all bits set? bool isSet( int index = -1 ) { if( index >= 0 ) { if( index < size() ) return _bits[ index / ( sizeof( quint32 ) * 8U ) ] & ( 1U << ( index % ( sizeof( quint32 ) * 8U ) ) ); } else { for( int i = 0, sizeInWords = size( true ); i < sizeInWords; i++ ) { if( _bits[ i ] != ~( 0U ) ) { return false; } } return true; } return false; } //index >= 0 && < sizeInBits -- is bit clear by index? // otherwise -- is all bits clear? bool isClear( int index = -1 ) { if( index >= 0 ) { if( index < size() ) return !( _bits[ index / ( sizeof( quint32 ) * 8U ) ] & ( 1U << ( index % ( sizeof( quint32 ) * 8U ) ) ) ); } else { for( int i = 0, sizeInWords = size( true ); i < sizeInWords; i++ ) { if( _bits[ i ] ) { return false; } } return true; } return false; }
Apoheliy
02.01.2023 00:43+3Возможно, в операции read если position кратен PAGE_SIZE и length равна PAGE_SIZE, то вы будете искать/читать 2 страницы вместо 1.
Возможно, вы очень оптимистичны, считая, что разница steady_timer time_point выдаёт наносекунды. Желательно использовать duration_cast (https://en.cppreference.com/w/cpp/chrono/duration/duration_cast);
Для хобби, возможно, это некритично:
[наверное что-то просмотрел и скопировать класс нельзя, но] лучше явно запретить копирование, а вот перенос можно бы и сделать.
гарантии на исключения вообще никакие. У вас bad_alloc может вылетать и ... ничего.
Basheyev Автор
02.01.2023 03:52+1Спасибо за советы! Про read страниц проверю, а вот про время duration_cast не знал. По исключениям подумаю как обрабатывать.
Basheyev Автор
03.01.2023 05:55Спасибо огромное за review! Действительно, страницы читались дважды при position кратном PAGE_SIZE и length равна PAGE_SIZE. Поправил. И касательно копирования - запретил. Обработку bad_alloc добавил.
Zara6502
03.01.2023 04:44-2в 2000 году был мне заказ сделать обработку данных на примере одной программы написанной очень крутым (для меня) программистом, который, по слухам, уехал в США и работал уже в Майкрософт.
Я знал только Delphi и LocalBDE и поэтому делал реализацию на них. У него проект был на C+WinAPI+какой-то бинарный формат БД.
Больше всего была разница в объемах файлов БД, у меня месяц работы выражался в 2-3 мегабайт (не гигабайт) данных, для транспортировки на одной дискете использовал ZIP, выгрузка сразу шла в архив при экспорте. А у гуру в прошлом проекте данные за год умещались в 200 килобайт и плохо паковались, подозреваю что он сам всё сжимал в какой-то LZ вариант. Тогда я понял, что мне не стать хорошим программистом XD (и не стал). А смотря на поделки современных разработчиков уверен что сделал бы отличную карьеру если бы пошел этим путём, ибо сегодня говнокодеры в почёте.
edo1h
03.01.2023 12:49что-то я сомневаюсь, что самописная БД в 2000 году — признак хорошего программиста
Zara6502
03.01.2023 17:20я люблю пользоваться решениями разработанными под конкретные задачи, универсальное не значит - всегда хорошее.
Maccimo
04.01.2023 05:28Первая версия SQLite появилась как раз в 2000 году. До неё со встраивыми СУБД было как-то не очень. Кто-то писал в пачку
*.dbf
, кто-то (ICQ, например) использовал формат MS Access.
edo1h
mysql, например, чтобы избежать двойного кэширования использует O_DIRECT
Tzimie
И MSSQL