Розробка магазину ключів / вартості, яка переходить на сучасний C ++


9

Я розробляю сервер баз даних, схожий на Cassandra.

Розробка була розпочата в С, але все стало дуже складним без занять.

Наразі я все портував на C ++ 11, але я все ще навчаюсь "сучасного" C ++ і маю сумніви щодо багатьох речей.

База даних буде працювати з парами ключ / значення. Кожна пара має додаткову інформацію - коли створюється також, коли вона закінчується (0, якщо не закінчується). Кожна пара незмінна.

Ключ - це рядок C, значення - недійсне *, але принаймні на даний момент я оперую зі значенням також як рядок 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(). Це важливо для мене, тому що якби я мав пари 1М, я мав би виділення 2М.

З іншого боку, якщо я виконаю "буфер" для динамічного масиву (наприклад, новий char [123]), я втрачу mmap "трюк", я зроблю два відхилення, якщо хочу перевірити ключ, і я додаю єдиний покажчик - 8 байт до класу.

Я також намагався "витягнути" всіх членів Pair::Blobзсередини Pair, Pair::Blobщоб бути лише буфером, але коли я тестував це, він був досить повільним, ймовірно, через копіювання даних об'єкта навколо.

Ще одна зміна, про яку я також думаю, - це видалити Pairклас і замінити його std::shared_ptrта "підштовхнути" всі методи назад Pair::Blob, але це не допоможе мені з Pair::Blobкласом змінних розмірів .

Мені цікаво, як я можу вдосконалити дизайн об'єкта, щоб бути більш сприятливим C ++.


Повний вихідний код тут:
https://github.com/nmmmnu/HM3


2
Чому ви не використовуєте std::mapабо std::unordered_map? Чому значення (пов'язані з ключами) деякі void*? Вам, певно, потрібно було б знищити їх у якийсь момент; як і коли? Чому ви не використовуєте шаблони?
Базиль Старинкевич

Я не використовую std :: map, тому що я вважаю (або принаймні намагаюся) зробити щось краще, ніж std :: map для поточного випадку. Але так, я думаю в якийсь момент обернути std :: map і перевірити його продуктивність як IList.
Нік

Розподіл та виклик d-tors проводиться там, де елемент є IList::removeабо коли знищено IList. Це займає багато часу, але я збираюся робити це окремо. Це буде легко, тому що IList все std::unique_ptr<IList>одно буде . тож я зможу "переключити" його з новим списком і зберегти старий об'єкт десь, де я можу викликати d-tor.
Нік

Я спробував шаблони. Тут вони не найкраще рішення, оскільки це не бібліотека користувачів, ключ є завжди, C stringа дані - це завжди якийсь буфер void *або char *, тож ви можете передавати масив char. Ви можете знайти подібні в redisабо memcached. У якийсь момент я міг вирішити використовувати std::stringабо виправлений масив char для ключа, але підкреслити це буде все-таки рядок C.
Нік

6
Замість того, щоб додавати 4 коментарі, слід відредагувати своє запитання
Basile Starynkevitch

Відповіді:


3

Я рекомендую підходити до того, щоб зосередити увагу на інтерфейсі вашої сховища ключових значень, щоб зробити його максимально чистим і максимально необмеженим, це означає, що він повинен забезпечувати максимальну свободу для абонентів, а також максимальну свободу вибору як це здійснити.

Тоді я рекомендую вам забезпечити якнайбільше та максимально чистою реалізацією, без жодних результатів. Мені здається, що це unordered_mapмає бути ваш перший вибір, або, можливо, mapякщо якесь впорядкування ключів повинно виставляти інтерфейс.

Отже, спочатку змусьте його працювати чисто і мінімально; потім, поставте його для використання в реальній програмі; роблячи це, ви знайдете, які проблеми потрібно вирішити в інтерфейсі; потім ідіть і звертайтеся до них. Більшість шансів на те, що в результаті зміни інтерфейсу вам потрібно буде переписати великі частини реалізації, тому будь-який час ви вже вклали кошти в першу ітерацію реалізації за межі мінімальної кількості часу, необхідної для її отримання ледь робота витрачається даремно.

Потім профіліруйте його і подивіться, що потрібно вдосконалити в реалізації, не змінюючи інтерфейс. Або у вас можуть бути свої уявлення про те, як поліпшити реалізацію, перш ніж ви навіть отримаєте профільний профіль. Це добре, але це все ще не є підставою працювати над цими ідеями в будь-який попередній момент часу.

Ви кажете, що сподіваєтесь зробити краще, ніж map; Про це можна сказати дві речі:

а) ви, мабуть, не будете;

б) уникати передчасної оптимізації за будь-яку ціну.

Що стосується реалізації, вашою основною проблемою, як видається, є розподіл пам’яті, оскільки ви, мабуть, переймаєтесь тим, як структурувати свій дизайн, щоб вирішити проблеми, які ви передбачаєте, що ви матимете стосовно розподілу пам’яті. Найкращий спосіб вирішити проблеми розподілу пам'яті в C ++ - це реалізувати відповідне управління розподілом пам’яті, а не закручуючи та згинаючи конструкцію навколо них. Вам слід пощастити, що ви використовуєте C ++, що дозволяє вам самостійно керувати розподілом пам’яті, на відміну від таких мов, як Java та C #, де ви майже затрималися з тим, що може запропонувати мова мовлення.

Існують різні способи управління пам'яттю в C ++, і можливість перевантажувати newоператора може стати в нагоді. Спрощений розподільник пам'яті для вашого проекту попередньо виділить величезний масив байтів і використає його як купу. ( byte* heap.) У вас був би firstFreeByteіндекс, ініціалізований до нуля, який вказує на перший вільний байт у купі. Коли Nнадходить запит на байти, ви повертаєте адресу heap + firstFreeByteта додаєте Nдо firstFreeByte. Отже, розподіл пам’яті стає настільки швидким та ефективним, що практично не стає проблемою.

Звичайно, попереднє розміщення всієї вашої пам’яті може виявитися не дуже хорошою ідеєю, тому вам, можливо, доведеться розбити купу в банки, які розподіляються на вимогу, і продовжувати обслуговувати запити на розподіл у найновішому банку в будь-який момент.

Оскільки ваші дані незмінні, це хороше рішення. Це дозволяє відмовитися від ідеї об'єктів змінної довжини і кожен повинен Pairмістити вказівник на свої дані як слід, оскільки додатковий розподіл пам'яті для даних практично не коштує.

Якщо ви хочете мати змогу викинути об'єкти з купи, щоб отримати можливість відновити їх пам’ять, тоді все ускладнюється: вам потрібно буде використовувати не покажчики, а покажчики на покажчики, щоб ви завжди могли переміщувати об’єкти навколо у купи, щоб повернути простір видалених об'єктів. Все стає трохи повільніше через додаткову непрямість, але все ще блискавично порівняно із використанням стандартних процедур розподілу пам'яті бібліотеки часу виконання.

Але все це, звичайно, марно турбуватися, якщо ви спочатку не побудуєте просту, голу мінімальну працюючу версію вашої бази даних і не використаєте її для використання в реальному додатку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.