如何避免在基于 B-tree 的类似 STL 的映射中浪费键复制?

Posted

技术标签:

【中文标题】如何避免在基于 B-tree 的类似 STL 的映射中浪费键复制?【英文标题】:How can I avoid wasteful copying of keys in a B-tree based STL-like map? 【发布时间】:2015-03-20 07:28:09 【问题描述】:

我将在热路径中使用 std::map 替换为 cpp-btree 的 btree_map。但是启用优化后,GCC 和 Clang 抱怨严格的别名违规。问题归结为:

template <typename Key, typename Value>
class btree_map 
public:
    // In order to match the standard library's container interfaces
    using value_type = std::pair<const Key, Value>;

private:
    using mutable_value_type = std::pair<Key, Value>;

    struct node_type 
        mutable_value_type values[N];
        // ...
    ;

public:
    class iterator 
        // ...

        value_type& operator*() 
            // Here we cast from const std::pair<Key, Value>&
            // to const std::pair<const Key, Value>&
            return reinterpret_cast<value_type&>(node->values[i]);
        
    ;

    std::pair<iterator, bool> insert(const value_type& value) 
        // ...
        // At this point, we have to insert into the middle of a node.
        // Here we rely on nodes containing mutable_value_type, because
        // value_type isn't assignable due to the const Key member
        std::move_backward(node->values + i, node->values + j,
                           node->values + j + 1);
        node->values[i] = value;
        // ...
    
;

这让我想到,有没有一种方法可以有效地做到这一点,而不依赖于未定义的行为?我使用的键可以有效地移动,但复制起来相当慢,所以我希望避免在每次插入时复制很多键。我考虑过

使用value_type values[N],然后使用const_cast&lt;Key&amp;&gt;(values[i].first) = std::move(key) 来移动键,但我很确定它仍然未定义 适当时返回std::pair&lt;const Key&amp;, Value&amp;&gt; 而不是std::pair&lt;const Key, Value&gt;&amp;,但我不确定这是否仍能满足容器要求(我听说...::reference 应该是真正的引用类型) 不在乎。代码按原样工作,但我很好奇它是否可以以符合标准的方式完成。未来的编译器也有可能对 UB 做不同的事情,我不知道有什么方法可以将 -fno-strict-aliasing 仅应用于单个类。

还有其他想法吗?

【问题讨论】:

Gcc 提供may_alias attribute,这与 -fno-strict-aliasing 类似,但仅在涉及具有 may_alias 属性的特定变量的表达式中。 是的。 (你的意思是may_alias 对吗?)我想知道是否有办法在没有语言扩展的情况下做到这一点。 你必须使用 ad-hoc C-array 吗?你必须使用对象数组还是可以使用指针数组? 仅供参考,在boost::flat_map 中,他们将value_type 定义为typedef std::pair&lt; Key, T &gt; @frymode 好点。我很想在界面中保留const Key 的额外安全性,但可能不实用。 【参考方案1】:

引用strict aliasing rules,

如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义:

...

在其成员中包含上述类型之一的聚合或联合类型(递归地包括子聚合或包含联合的成员),...

因此从 std::pair 到 std::pair 通过中间转换到联合或包含这两种类型作为成员的结构不会违反严格的别名规则。

警告:直到 C++11 才允许联合中的 std::pair,可以使用结构代替。

警告:假设这两种类型具有兼容的布局可能不成立。想象一下根据 Key 类型的 constness 对 first 和 second 进行不同排序的实现。

【讨论】:

我可以定义我自己的pair&lt;&gt; 类型来确保布局。但即便如此,像reinterpret_cast&lt;A&amp;&gt;(reinterpret_cast&lt;ABUnion&amp;&gt;(b)) 这样的演员并不能真正拯救你吗?最后,您仍然通过A 类型的左值访问B 类型的对象。 @TavianBarnes 不对联合引用应用另一个类型转换,而是使用字段访问。这是 c11/c++11 之前的未指定行为,c11 保证它可以正常工作。不确定使用结构而不是联合是否可以,可能不是。 好的,好点。如果我对标准的解读是准确的,那么只要pair&lt;Key, Value&gt;pair&lt;const Key, Value&gt; 有一个包含firstsecond公共初始子序列,那么这就是明确的定义,这只有在@987654330 时才有可能@ 和 Value 本身是标准布局类型。所以它适用于btree_map&lt;int, int&gt;,但不一定适用于btree_map&lt;std::string, std::string&gt;【参考方案2】:

修改:将 move_backward(...) 扩展为 for 循环,显式调用析构函数并放置 new 以避免赋值错误。

Placement new 可以用来代替简单的赋值。

注意:下面的这个实现不是异常安全的。异常安全需要额外的代码。

template <typename Key, typename Value>
class btree_map 
// ...
private:
    struct node_type 
        // Declare as value_type, not mutable_value_type.
        value_type values[N];
        // ...
    ;

    class iterator 
        // ...

        value_type& operator*() 
            // Simply return values[i].
            return node->values[i];
        
    ;

    std::pair<iterator, bool> insert(const value_type& value) 
        // ...
        // expand move_backward(...)
        for(size_t k = j + 1; k > i; k--) 
            // Manually delete the previous value prior to assignment.
            node->values[k].~value_type();
            // Assign the new value by placement new.
            // Note: it goes wrong if an exception occurred at this point.
            new(&node->values[k]) value_type(node->values[k - 1]);
        
        // Matual delete and placement new too.
        node->values[i].~value_type();
        // Note: it goes wrong if an exception occurred at this point.
        new (&node->values[i]) value_type(value);
        // ...
    
;

【讨论】:

std::move_backward 无法编译,因为value_type 不可赋值 抱歉,std::move_backward 应替换为放置新循环。我修复了代码。 该循环将进行复制构造,而不是移动构造。即使您添加std::move(node-&gt;values[k - 1]),它仍然会复制密钥,因为value_type 的移动构造函数无法从const 字段first 移动。【参考方案3】:

你不要无缘无故地使用 BTREE 作为替代平衡二叉树:通常是因为你有一个慢得多的物理存储并且作为块设备工作,你需要在一个节点中使用一个数组,“有点" 表示设备块。所以试图优化处理内部数组所花费的周期是非常微不足道的。

但是,我怀疑 N 是关键因素:

struct node_type 
    mutable_value_type values[N];
    // ...
;

如果它足够小,您可能不需要按键顺序插入/查找/删除元素。性能可能比尝试将它们排列在一个小数组中更好。为避免在 Remove 函数中发生任何移动,您还可以定义一个空槽,这样 Remove 只会用空槽替换一个元素,而 Insert 会用一个元素替换第一个遇到的空槽。

对于更大的数组,您可以使用两个数组:一个将包含一对作为元素,其中包含一个键和一个指向存储在第二个数组中的值的索引/迭代器。这样,无论value_type 的类型如何,您仍然可以快速对第一个数组进行排序。也就是说,在插入或删除元素时,您仍然需要处理第二个数组。在创建节点时,第一个数组还可能包含具有第二个数组中预分配索引的特殊对。在删除一个元素时还会设置一个特殊的对(保留索引以供以后插入另一个元素)。在对第一个数组进行排序时,这些特殊对将放在最后。并且知道插入元素的数量,您可以将其用作索引来分配第一个特殊对以在 O(1) 处插入一个元素(在第二个数组中分配)。在删除一个元素时,一个特殊的对可以在排序之前替换第一个数组中的正常对(保留索引)。您只需要对当前元素(插入或删除)使用放置新调用或析构函数调用。

【讨论】:

我怀疑在这种情况下主内存中的“慢”外部存储。由于更高的缓存效率和更少的指针追逐,BTree 在现代系统中可以比二叉树更快。 如果一对的大小足够小以允许缓存中的多个元素,则为 true。即便如此,对内部数组进行排序可能也没有必要。我的意思是,使用 std::vector&lt; std::pair&lt; key, value &gt; &gt; 而不是 std::map&lt; key, value &gt; 可能会更快,因为元素的数量很少。

以上是关于如何避免在基于 B-tree 的类似 STL 的映射中浪费键复制?的主要内容,如果未能解决你的问题,请参考以下文章

OceanBase存储引擎核心-LSM Tree VS B-tree

寻找类似 C++ STL 的向量类但使用堆栈存储

Linq b-tree(b +,b *无论如何)有关DB的操作数集合?

关于QtCharts中的映射器与模型的使用

如何创建从实体到 dto 的映射器,其中 dto 嵌套在哪里?

Swift 基于 B-Tree 的集合类型类库 BTree