如何避免在基于 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<Key&>(values[i].first) = std::move(key)
来移动键,但我很确定它仍然未定义
适当时返回std::pair<const Key&, Value&>
而不是std::pair<const Key, Value>&
,但我不确定这是否仍能满足容器要求(我听说...::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< Key, T >
@frymode 好点。我很想在界面中保留const Key
的额外安全性,但可能不实用。
【参考方案1】:
引用strict aliasing rules,
如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义:
...
在其成员中包含上述类型之一的聚合或联合类型(递归地包括子聚合或包含联合的成员),...
因此从 std::pair
警告:直到 C++11 才允许联合中的 std::pair,可以使用结构代替。
警告:假设这两种类型具有兼容的布局可能不成立。想象一下根据 Key 类型的 constness 对 first 和 second 进行不同排序的实现。
【讨论】:
我可以定义我自己的pair<>
类型来确保布局。但即便如此,像reinterpret_cast<A&>(reinterpret_cast<ABUnion&>(b))
这样的演员并不能真正拯救你吗?最后,您仍然通过A
类型的左值访问B
类型的对象。
@TavianBarnes 不对联合引用应用另一个类型转换,而是使用字段访问。这是 c11/c++11 之前的未指定行为,c11 保证它可以正常工作。不确定使用结构而不是联合是否可以,可能不是。
好的,好点。如果我对标准的解读是准确的,那么只要pair<Key, Value>
和pair<const Key, Value>
有一个包含first
和second
的公共初始子序列,那么这就是明确的定义,这只有在@987654330 时才有可能@ 和 Value
本身是标准布局类型。所以它适用于btree_map<int, int>
,但不一定适用于btree_map<std::string, std::string>
。【参考方案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->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< std::pair< key, value > >
而不是 std::map< key, value >
可能会更快,因为元素的数量很少。以上是关于如何避免在基于 B-tree 的类似 STL 的映射中浪费键复制?的主要内容,如果未能解决你的问题,请参考以下文章
OceanBase存储引擎核心-LSM Tree VS B-tree
Linq b-tree(b +,b *无论如何)有关DB的操作数集合?