Я разрабатываю сервер базы данных, похожий на Cassandra.
Разработка была начата в C, но все стало очень сложно без классов.
В настоящее время я перенес все на C ++ 11, но я все еще изучаю "современный" C ++ и у меня есть сомнения по поводу многих вещей.
База данных будет работать с парами ключ / значение. У каждой пары есть еще какая-то информация - когда создается и когда истекает (0, если не истекает). Каждая пара неизменна.
Ключ - строка C, значение - void *, но, по крайней мере, на данный момент я работаю со значением как строка C.
Есть абстрактный IList
класс. Наследуется от трех классов
VectorList
- C динамический массив - аналогичен std :: vector, но используетrealloc
LinkList
- сделано для проверки и сравнения производительностиSkipList
- класс, который, наконец, будет использоваться.
В будущем я мог бы также сделать Red Black
дерево.
Каждый IList
содержит ноль или более указателей на пары, отсортированные по ключу.
Если IList
стало слишком долго, его можно сохранить на диске в специальном файле. Этот специальный файл является своего рода read only list
.
Если вам нужно найти ключ,
- сначала в памяти
IList
ищется (SkipList
,SkipList
илиLinkList
). - Затем поиск отправляется по файлам, отсортированным по дате
(сначала самый новый файл, последний файл - последний).
Все эти файлы хранятся в памяти. - Если ничего не найдено, то ключ не найден.
У меня нет сомнений в реализации IList
вещей.
То, что меня сейчас озадачивает, таково:
Пары имеют разный размер, они выделены, new()
и они std::shared_ptr
указали на них.
class Pair{
public:
// several methods...
private:
struct Blob;
std::shared_ptr<const Blob> _blob;
};
struct Pair::Blob{
uint64_t created;
uint32_t expires;
uint32_t vallen;
uint16_t keylen;
uint8_t checksum;
char buffer[2];
};
Переменная-член «буфер» - это переменная другого размера. Он хранит ключ + значение.
Например, если ключ - 10 символов, а значение - еще 10 байтов, весь объект будет sizeof(Pair::Blob) + 20
(начальный размер буфера равен 2 из-за двух нулевых завершающих байтов)
Эта же схема используется и на диске, поэтому я могу сделать что-то вроде этого:
// get the blob
Pair::Blob *blob = (Pair::Blob *) & mmaped_array[pos];
// create the pair, true makes std::shared_ptr not to delete the memory,
// since it does not own it.
Pair p = Pair(blob, true);
// however if I want the Pair to own the memory,
// I can copy it, but this is slower operation.
Pair p2 = Pair(blob);
Однако этот другой размер является проблемой во многих местах с кодом C ++.
Например я не могу использовать std::make_shared()
. Это важно для меня, потому что, если у меня есть 1M пар, у меня будет 2M.
С другой стороны, если я сделаю «буфер» для динамического массива (например, новый символ [123]), я потеряю «трюк» mmap, мне придется сделать две разыменования, если я хочу проверить ключ и добавлю один указатель - 8 байтов в класс.
Я также пытался «вытащить» все элементы Pair::Blob
изнутри Pair
, чтобы Pair::Blob
быть просто буфером, но когда я тестировал его, он был довольно медленным, вероятно, из-за копирования данных объекта.
Еще одно изменение, о котором я также думаю, - это удалить Pair
класс и заменить его на std::shared_ptr
и «протолкнуть» все методы обратно Pair::Blob
, но это не поможет мне с Pair::Blob
классом переменного размера .
Мне интересно, как я могу улучшить дизайн объекта, чтобы быть более дружественным к C ++.
Полный исходный код здесь:
https://github.com/nmmmnu/HM3
std::map
илиstd::unordered_map
? Почему значения (связанные с ключами) некоторыеvoid*
? Возможно, вам придется уничтожить их в какой-то момент; как и когда? Почему вы не используете шаблоны?IList::remove
или когда IList разрушен. Это займет много времени, но я собираюсь сделать это в отдельной теме. Это будет легко, потому что IList будет вstd::unique_ptr<IList>
любом случае. так что я смогу «поменять» его новым списком и сохранить старый объект где-нибудь, где я могу вызвать d-tor.C string
а данные всегда некоторый буферvoid *
илиchar *
, таким образом, вы можете передать массив символов. Вы можете найти подобное вredis
илиmemcached
. В какой-то момент я мог бы решить использоватьstd::string
или исправить массив символов для ключа, но подчеркнуть, что это все еще будет строка C.Ответы:
Подход, который я бы порекомендовал, заключается в том, чтобы сосредоточиться на интерфейсе вашего хранилища значений ключей, чтобы сделать его максимально чистым и максимально неограниченным, а это означает, что он должен обеспечивать максимальную свободу для вызывающих, а также максимальную свободу выбора как это реализовать.
Затем я бы порекомендовал вам предоставить как можно более чистую и максимально чистую реализацию без каких-либо проблем с производительностью. Мне кажется, это
unordered_map
должен быть ваш первый выбор, или, можетmap
быть, интерфейс должен демонстрировать какой-то порядок ключей.Итак, сначала заставьте его работать чисто и минимально; затем примените его в реальном приложении; при этом вы найдете, какие проблемы вам нужно решить на интерфейсе; тогда, продолжайте и обратитесь к ним. Скорее всего, в результате изменения интерфейса вам потребуется переписать большие части реализации, поэтому каждый раз, когда вы уже инвестировали в первую итерацию реализации, вы тратите минимум времени, необходимого для того, чтобы он едва работа - это потеря времени.
Затем профилируйте его и посмотрите, что нужно улучшить в реализации, не изменяя интерфейс. Или у вас могут быть свои собственные идеи о том, как улучшить реализацию, прежде чем вы даже профиль. Это нормально, но пока нет оснований работать над этими идеями в любой более ранний момент времени.
Вы говорите, что надеетесь сделать лучше, чем
map
; Об этом можно сказать две вещи:а) вы, вероятно, не будете;
б) избегать преждевременной оптимизации любой ценой.
Что касается реализации, вашей основной проблемой, по-видимому, является распределение памяти, так как вы, кажется, обеспокоены тем, как структурировать свой дизайн, чтобы обойти проблемы, которые вы предвидите, которые у вас возникнут в отношении распределения памяти. Лучший способ решить проблемы с выделением памяти в C ++ - это реализовать подходящее управление распределением памяти, а не путать и изгибать конструкцию вокруг них. Вам следует считать себя счастливым, если вы используете C ++, который позволяет вам самостоятельно управлять распределением памяти, в отличие от таких языков, как Java и C #, где вы в значительной степени застряли на том, что может предложить языковая среда выполнения.
Существуют различные способы управления памятью в C ++, и
new
может оказаться полезной возможность перегрузки оператора. Упрощенный распределитель памяти для вашего проекта будет предварительно выделять огромный массив байтов и использовать его в качестве кучи. (byte* heap
.) У вас будетfirstFreeByte
индекс, инициализированный нулем, который указывает первый свободный байт в куче. Когда приходит запрос наN
байты, вы возвращаете адресheap + firstFreeByte
и добавляетеN
вfirstFreeByte
. Таким образом, распределение памяти становится настолько быстрым и эффективным, что практически не становится проблемой.Конечно, предварительное выделение всей вашей памяти может быть не очень хорошей идеей, поэтому вам, возможно, придется разбить вашу кучу на банки, которые распределяются по требованию, и продолжать обслуживать запросы на выделение из самого нового в данный момент банка.
Поскольку ваши данные неизменны, это хорошее решение. Это позволяет отказаться от идеи объектов переменной длины, и каждый из них должен
Pair
содержать указатель на свои данные, как и положено, поскольку дополнительное выделение памяти для данных практически ничего не стоит.Если вы хотите иметь возможность удалять объекты из кучи, чтобы иметь возможность восстановить их память, то все становится более сложным: вам нужно будет использовать не указатели, а указатели на указатели, чтобы вы всегда могли перемещать объекты вокруг в кучах, чтобы восстановить пространство удаленных объектов. Все становится немного медленнее из-за дополнительной косвенности, но все по-прежнему молниеносно по сравнению со стандартными процедурами выделения памяти во время выполнения библиотеки.
Но все это, конечно, бесполезно, если вы сначала не создадите простую, минимально работающую версию своей базы данных и не применили ее в реальном приложении.
источник