【问题标题】:B+ tree implementation, * * vs *B+ 树实现,* * vs *
【发布时间】:2009-09-11 02:22:32
【问题描述】:

出于各种原因,我正在编写 B+ 树,我来这里是为了询问有关其节点实现的问题。我的节点目前看起来像:

struct BPlusNode
{
public:
    //holds the list of keys
    keyType **keys;
    //stores the number of slots used
    size_t size;
    //holds the array of pointers to lower nodes NULL if this is a leaf node
    BPlusNode **children;
    //holds the pointer to the next load to the 'left'
    BPlusNode *next;
    //Data page pointers NULL if this is a branch node
    Bucket **pages;
};

如您所见,我当前的实现是在我想知道是否应该使用 * * 或 * 的地方使用 *。

我很清楚 * * 需要两个取消引用操作,因此比简单地使用 * 慢,但是这个类使用了大量的递归,并且将指针传递给递归的子调用要方便得多职能。要使用 * 执行此操作,我需要进行指针运算并传递结果指针。

带**

someFunction(BPlusNode* currNode)
{
    ......
    someFunction(currNode->children[ChildIndex]);
}



带 *

someFunction(BPlusNode* currNode)
{
    ......
    someFunction((currNode->children) + ChildIndex);
}

我可以看到在 * * 版本中需要额外读取内存以生成所需的指针,但 * * 版本对我来说也更容易考虑(它更符合我对绘制图表的看法在“计算机编程艺术”和维基百科中)。

有没有人有任何想法?对第三种选择的建议?证明为什么一个优于另一个?等等?

编辑:
我可能会将其发布为下面的答案,但我刚刚意识到使用 * * 方案我不需要复制每个子节点或存储桶的全部内容,如果我想将一个插入到数组的中间(即调整数组的大小) .如果在我重新分配数组时 * 方案有 20 个子节点,我需要复制 20*sizeof(BPlusNode) 字节,而不是 * * 方案的 20*sizeof(BPlusNode*) 字节。

另一方面,我想到,由于我执行所有插入和页面拆分都是预先完成的,因此执行它们的效率提高可能是不必要的,并且在搜索中 * over * * 的好处超过了它。

【问题讨论】:

  • 由于这是标记为 C++,是否有某些原因您不能通过引用传递指针而不是进行指针运算?
  • 据我了解,它做一些类似 someFunction(BPlusNode*& currNode).... 然后通过 someFunction(currNode->children[ChildIndex]) 调用它,会比 * * 更糟糕。 [] 与 *(currNode->children + ChildIndex) 基本相同,因此,与 * * 方案相同的是指针运算,然后是取消引用。与 * * 方案不同,对象的 this 指针必须被检索并传递。所以在我看来,在效率上,它至少相当于**方案。也许更糟。
  • @James:我确定 grayfade 建议使用 someFunction(BPlusNode& currNode) 的签名。这在功能上(在性能方面)与someFunction(BPlusNode* currNode) 相同,但看起来更简洁,并且避免了意外更改指针(而不是指针对象)可能导致的错误。

标签: c++ data-structures pointers


【解决方案1】:

我将为键和指针数据定义另一个结构。我会承诺使用与您的磁盘结构相匹配的固定大小的节点。这使得内存映射树变得更加容易。

你的 BPlusNode 结构变成了一个句柄类,它指向这些映射的数据节点,并通过在树的下行过程中读取兄弟姐妹来合成诸如 prev 和 next 指针之类的东西。

它可能看起来像下面这样:

enum BPlusNodeType {
    LEAF, BRANCH
};

struct BPlusNodeData {
    static const size_t max_size = 511; // Try to fit into 4K? 8K?
    uint16_t size;
    uint16_t type;
    keyType key[max_size];
    union {
        Bucket* data[max_size];
        BPlusNodeData* children[max_size];
    };
};

【讨论】:

  • +1。与基于*** 的解决方案相比,固定大小的就地节点指针数组将更快、更干净(不会忘记动态内存(de/re)分配)。
  • 我更喜欢这里的优雅,但我对语法有点模糊。我知道联合在第二种情况下是如何工作的,但是第一个联合声明如何适应任何事情?
  • 另外,如果我告诉你 keyType 本身就是一个丑陋的东西,会对你如何做这件事有什么影响。不幸的是,keyType 实际上是我称之为 numericArray 的东西,它本质上是一个动态数组,用于存储 1 到 n 维坐标,然后将这些坐标转换为具有希尔伯特空间填充曲线的 1D 索引(使用一些按位魔法)。这会影响您的工作方式吗?
  • 在定义 BPlusNodeType 时,也许您的意思是枚举而不是联合?
  • @James:是的,我确定他的意思是enum。我建议对他进行-1 以引起他的注意。
【解决方案2】:

使用**,您需要一个额外的分配步骤来保存每个BPlusNode* 子指针。或者您可以分配其中的一个块,并让children 中的每个指针指向该块内的连续BPlusNode* 元素——但它仍然是每个节点创建的一个额外动态内存分配(以及销毁时相应的额外释放步骤) .所以我绝对推荐使用单个*。如果写

someFunction((currNode->children) + ChildIndex);

伤了你,你可以改写成

someFunction(&currNode->children[ChildIndex]);

我觉得更清楚。

【讨论】:

  • 但是,这样做与进行指针运算,取消引用运算结果,然后检索节点的 this 指针不一样吗?另外,我只需要一口气对树进行插入,实际上当前数据集将有大约 25.57 亿次插入调用(尽管由于页面结构只有大约 255,700 个桶)。由于这棵树只有 256 阶,在最初的匆忙中会有很多分裂,所以,我们不会从调整数组大小的便利性中受益吗?
  • 我不确定您的确切意思——我发布的两个代码 sn-ps 在各个方面在功能上都相同,并且将生成完全相同的代码他们。第二个 sn-p 中的 & 防止指针实际被取消引用——只有指针运算发生以产生地址作为最终结果。 (并且您意识到您的 ** 版本也执行相同的指针运算,对吗?然后进行额外的指针取消引用。)此外,在任何阶段都没有涉及任何 this 指针,我对您的意思感到困惑在那里。
  • 好吧,我误解了 &(something[index]) 发生的事情,我认为它执行了取消引用,然后不得不回去找到指向结构的指针。但是插入期间的效果如何?不必重新分配和复制数组中需要调整大小的每个节点?
  • 中间的插入是喜欢**的原因之一。但我认为 B 树的想法是它们在分裂之前只增长到(相当少)固定数量的子节点——对吗?如果是这样,并且如果该阈值小于几百个节点,我敢打赌,在中间插入时复制节点指针仍然更快。 memcpy() 很快;每个子插入的动态内存分配不是(而且还会使您的内存需求增加一倍以上,因为每次分配都需要存储 4-16 字节的内务信息)。
  • @j_random_hacker:我发现太多的数据库喜欢复制索引数据。使用您自己的 BTree,只有一个副本。当然,有些数据库确实提供了没有表的仅索引“表”,但我记得我发现的所有表都很昂贵。
【解决方案3】:

使用 STL 'vector<keyType *> keys' 和 'vector<BPlusNode *> children' 等会更好吗?

这可能太简单了,但我的印象是 C++ 中并不经常需要双重间接(在 C 中并不经常需要,但比 C++ 更频繁)。

【讨论】:

  • 使用 STL 添加会进一步减慢速度。该程序必须处理来自索引的大量读取(甚至更多来自磁盘的读取)。我希望对这棵树的查询尽可能快。
  • 最后,使用 std::vector 等使节点变大。 Vector 有一些与之相关的空间开销,虽然当我只有几十亿个事件,因此只有几十万页和一千个节点左右时,这不是一个问题,但这并不是那么糟糕(尽管 keyType 采用自行增加大量空间)。但是,当我们看到有几千亿或更糟糕的几万亿事件(有时是必要的数量)的情况时,就会发现尽可能少的内存开销是最好的。
  • @James:读/写vector<keyType*> 不会比读/写动态分配的keyType* 数组慢——任何体面的编译器都会为两者生成相同的代码。 OTOH vector 空间开销是真实的(尽管我怀疑是否重要)。通常我会提倡用vector 替换任何固定大小或动态分配的数组,但在这种情况下,我认为采用 Zan Lynx 的固定大小数组方法是合适的,它会更小更快。跨度>
猜你喜欢
  • 1970-01-01
  • 2013-10-08
  • 1970-01-01
  • 2013-04-24
  • 2012-12-12
  • 1970-01-01
  • 2013-12-17
  • 1970-01-01
  • 2011-02-06
相关资源
最近更新 更多