糟糕的树增加了性能

Poor tree add performance

本文关键字:性能 增加      更新时间:2023-10-16

我现在正在编写一个树容器(只是为了理解和训练(,到目前为止,我得到了第一个非常基本的方法来向树中添加元素。

这是我知道的树代码。现在没有析构函数,没有清理,也没有元素访问。

template <class T> class set
{
public:
struct Node
{
Node(const T& val)
: left(0), right(0), value(val)
{}
Node* left;
Node* right;
T     value;
};
set()
{}
template <class T> void add(const T& value)
{
if (m_Root == nullptr)
{
m_Root = new Node(value);
}
Node* next    = nullptr;
Node* current = m_Root;
do
{
if (next != nullptr)
{
current = next;
}
next = value >= current->value ? current->left : current->right;
} while (next != nullptr);
value >= current->value ? current->left = new Node(value) : current->right = new Node(value);
}
private:
Node* m_Root;
};

好吧,现在我用唯一且平衡(低和高(的值测试了 std::set 的插入性能,得出的结论是性能太糟糕了。

集合插入值的速度如此之快,有什么理由可以提高我的方法的插入性能?(我知道可能有更好的树模型,但据我所知,大多数树模型之间的插入性能应该很接近(。

在 i5 4570 库存时钟下, std::set 需要 0.013s 才能添加 1000000 个 int16 值。 我的集合需要 4.5 秒才能添加相同的值。

这种巨大的差异从何而来?

更新:

好的,这是我的测试代码:

int main()
{
int n = 1000000;
test::set<test::int16> mset; //my set
std::set<test::int16>   sset; //std set
std::timer      timer;         //simple wrapper for clock()
test::random_engine engine(0, 500000); //simple wrapper for rand() and yes, it's seeded, and yes I am aware that an int16 will overflow
std::set<test::int16> values; //Set of values to ensure unique values
bool flip = false;
for (int i = 0; n > i; ++i)
{
values.insert(flip ? engine.generate() : 0 - engine.generate());
flip = !flip; //ensure that we get high and low values and no straight line, but at least 2 paths
}
timer.start();
for (std::set<test::int16>::iterator it = values.begin(); values.end() != it; ++it)
{
mset.add(*it);
}
timer.stop();
std::cout << timer.totalTime() << "s for msetn";
timer.reset();
timer.start();
for (std::set<test::int16>::iterator it = values.begin(); values.end() != it; ++it)
{
sset.insert(*it);
}
timer.stop();
std::cout << timer.totalTime() << "s for stdn";
}

由于 dubicates 的原因,该集合不会存储每个值,但两个容器将获得一个较高的数字和相同的值,以确保具有代表性的结果。我知道测试可能更准确,但它应该给出一些可比的数字。

std::set实现通常使用红黑树数据结构。它是一个自平衡的二叉搜索树,它的insert操作保证在最坏的情况下(这是标准要求(O(log(n))时间复杂度。您可以使用简单的二叉搜索树O(n)最坏情况的插入操作。

如果插入唯一的随机值,那么如此大的差异看起来很可疑。但不要忘记,随机性不会让你的树平衡,树的高度可能比log(n)大得多

编辑

似乎我发现了您的代码的主要问题。所有生成的值都存储在std::set中。之后,按递增顺序将它们添加到集合中。这会将您的集合降级到链表。

两个明显的区别是:

  1. std::set中使用的红黑树(可能(重新平衡自己,为最坏情况的行为设置上限,正如DAle所说。

    如果这是问题所在,则在绘制 N(插入的节点数(与每次插入时间时,您应该看到它。您还可以跟踪树的深度(至少出于调试目的(,并将其绘制成针对 N 的图。

  2. 标准容器使用分配器,它可能比单独new每个节点更聪明。您可以尝试在自己的容器中使用std::allocator,看看这是否会带来重大改进。


编辑 1如果实现了池分配器,则问题中应包含相关信息。

编辑 2现在您已经添加了测试代码,有一个明显的问题,这意味着您的集合将始终具有最差的插入性能。您预先排序了输入值!std::set是一个有序容器,所以首先把你的值放在那里可以保证你总是以递增的值顺序插入,所以你的树(不自平衡(退化为一个昂贵的链表,你的插入总是线性的,而不是对数时间。

您可以通过将值存储在vector中(仅使用set检测冲突(或使用unordered_set进行重复数据删除而无需预排序来验证这一点。