密钥/价值商店开发移植到现代C ++


9

我正在开发类似于Cassandra的数据库服务器。

开发从C语言开始,但是没有类,事情变得非常复杂。

目前,我在C ++ 11中移植了所有内容,但我仍在学习“现代” C ++,并对很多事情有疑问。

数据库将使用键/值对。每对都有更多信息-什么时候创建以及何时过期(如果不过期则为0)。每对都是不可变的。

关键是C字符串,值是空*,但至少目前我也使用C字符串作为值。

有抽象IList类。它从三个类继承

  • VectorList -C动态数组-类似于std :: vector,但使用 realloc
  • LinkList -用于检查和性能比较
  • SkipList -最终将使用的类。

将来我可能Red Black也会做树。

每个都IList包含零个或多个指向对的指针,并按键排序。

如果IList太长,可以将其保存在磁盘上的特殊文件中。这个特殊文件是的read only list

如果您需要搜索密钥,

  • 首先在存储器IList中搜索(SkipListSkipListLinkList)。
  • 然后将搜索发送到按日期排序的文件
    (最新的文件在前,最早的文件在后)。
    所有这些文件都在内存中映射。
  • 如果未找到任何内容,则找不到密钥。

我对事情的执行毫无疑问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];
};

“缓冲区”成员变量是具有不同大小的成员变量。它存储键+值。
例如,如果key是10个字符,并且value是另外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个分配。

另一方面,如果我对动态数组执行“缓冲”操作(例如new char [123]),我将丢失mmap“技巧”,如果要检查键,将进行两次取消引用,并添加单个指针-类的8个字节。

我还尝试将所有成员从中“拉” Pair::BlobPair,因此Pair::Blob只是缓冲区,但是当我对其进行测试时,它的速度很慢,可能是因为复制了对象数据。

我也在想的另一个更改是删除Pair类并替换为,std::shared_ptr然后将所有方法“推”回Pair::Blob,但这对可变大小的Pair::Blob类没有帮助。

我想知道如何才能改进对象设计,以便对C ++更加友好。


完整的源代码在这里:https :
//github.com/nmmmnu/HM3


2
为什么不使用std::mapstd::unordered_map?为什么值(与键相关)有些void*?您可能需要在某个时候销毁它们。怎样,几时?您为什么不使用模板?
Basile Starynkevitch 2015年

我不使用std :: map,因为在当前情况下,我相信(或至少尝试)做得比std :: map更好。但是,是的,我正在考虑包装std :: map并将其作为IList进行性能检查。
尼克

释放分配和调用d-tor是在element所在的位置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 2015年

Answers:


3

我建议的方法是专注于键值存储的界面,以使其尽可能整洁并尽可能不受限制,这意味着它应允许调用者获得最大的自由度,但也应获得最大的选择自由度如何实施。

然后,我建议您提供一个尽可能裸露且尽可能干净的实现,而绝不涉及任何性能问题。在我看来,这unordered_map应该是您的首选,或者map如果接口必须公开某种按键排序方式。

因此,首先要使其干净整洁地工作;然后,将其用于实际应用中;这样,您将在界面上找到需要解决的问题;然后,继续解决。多数机会是,由于更改了接口,您将需要重写实现的大部分内容,因此,只要您已经花了很少的最短时间就已经对实现的第一次迭代进行了投入,几乎没有时间在浪费时间。

然后,对其进行概要分析,并在不更改接口的情况下查看需要在实现中进行哪些改进。或者,甚至在进行概要分析之前,您可能会对改进实施有自己的想法。很好,但是仍然没有理由在任何较早的时间点上研究这些想法。

你说你希望比更好map; 关于这一点,可以说两件事:

a)您可能不会;

b)不惜一切代价避免过早的优化。

关于实现,您的主要问题似乎是内存分配,因为您似乎关心如何结构化设计以解决预期的内存分配问题。解决C ++中内存分配问题的最佳方法是实施适当的内存分配管理,而不是围绕它们进行扭曲和弯曲。您应该感到自己很幸运,因为您正在使用C ++,它使您能够进行自己的内存分配管理,而与Java和C#这样的语言相对,后者在很大程度上与语言运行时所提供的功能息息相关。

使用C ++进行内存管理的方式有很多种,使new操作员过载的功能可能派上用场。为您的项目使用简单的内存分配器将预分配大量字节,并将其用作堆。(byte* heap。)您将有一个firstFreeByte索引,初始化为零,该索引指示堆中的第一个空闲字节。收到N字节请求时,您返回地址heap + firstFreeByte并添加N到中firstFreeByte。因此,内存分配变得如此快速和高效,以至于几乎没有问题。

当然,预先分配所有内存可能不是一个好主意,因此您可能必须将堆拆分成按需分配的存储区,并继续满足当前任何最新存储区的分配请求。

由于您的数据是不可变的,因此这是一个很好的解决方案。它允许您放弃可变长度对象的概念,并让每个对象都Pair包含一个指向其数据的指针,因为为数据分配额外的内存实际上不花费任何费用。

如果您希望能够从堆中丢弃对象,以便能够回收它们的内存,那么事情就会变得更加复杂:您将需要使用的不是指针,而是指针的指针,以便您始终可以移动对象在堆中,以回收已删除对象的空间。由于使用了额外的间接寻址,因此一切都会变慢,但是与使用标准的运行时库内存分配例程相比,一切仍然快如闪电。

但是,如果您不首先构建数据库的直接,最小的工作版本,并在实际应用程序中使用它,那么所有这些当然毫无用处。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.