【发布时间】:2021-09-02 14:30:25
【问题描述】:
我需要在我的 C++ 代码中多次遍历一棵树,树的深度可能因一次迭代而异。我也可能有条件地从树遍历中提前中断。在分析我的代码时(使用 Visual Studio 编译器),我注意到树遍历部分是我代码中最大的瓶颈,因此我需要尽可能加快该部分的速度。
下面是我的代码的描述和简化的可运行版本,以展示我目前遇到的问题。
在使用递归时,我注意到我可以通过有条件地从递归中提前中断来加速我的代码。但是,我对早断的实现根本没有提高速度(参见代码)。我认为通过使用循环而不是递归,提前中断会更容易实现,所以我将树遍历转换为循环。令人惊讶的是,循环版本比递归版本慢了一个数量级!此外,early-break 最多只提高了 10% 的速度,这令人惊讶,因为这是深度优先搜索遍历,当中断发生时,树的很大一部分没有被遍历。因此,预计至少有 50-100% 的加速。
我的问题:
- 为什么在特定情况下,循环版本的顺序是 幅度变慢?!
- 为什么提前中断并不能大大提高速度(循环和递归)
- 非常感谢针对以下案例的任何其他性能提示。
#include <iostream>
#include <vector>
#include <stack>
#include <chrono>
using namespace std;
using namespace std::chrono;
class Node {
public:
int id;
int left = -1;
int right = -1;
int count = 0;
Node(int _id) { id = _id; }
};
std::vector<Node> nodes;
//1) recursive tree traversal
void recursive(int node) {
if (nodes[node].left == -1) {
nodes[node].count++;
}
else {
recursive(nodes[node].right);
recursive(nodes[node].left);
}
}
//2) recursive tree traversal with conditional break
void recursive2(int node, bool* stop) {
if (*stop == false) {
if (nodes[node].left == -1) {
nodes[node].count++;
if (rand() % 2 == 0) { *stop = true; } //conditional break
}
else {
recursive2(nodes[node].right, stop);
if (*stop == false) {
recursive2(nodes[node].left, stop);
}
}
}
}
// loop traversal
void loop(int node) {
stack<int> stack;
stack.push(node);
while (stack.size() > 0) {
node = stack.top();
stack.pop();
if (nodes[node].left == -1) {
nodes[node].count++;
//if (rand() % 2 == 0) { break; } // conditional break
}
else {
stack.push(nodes[node].right);
stack.push(nodes[node].left);
}
}
}
int main()
{
for (int i = 0; i < 7; i++) {
nodes.push_back(Node(i));
}
// make a simple tree /node 6 is the root
nodes[4].left = nodes[0].id;
nodes[4].right = nodes[1].id;
nodes[5].left = nodes[2].id;
nodes[5].right = nodes[3].id;
nodes[6].left = nodes[4].id;
nodes[6].right = nodes[5].id;
/// speed comparison
int n = 10000000;
int root_node = 6;
auto start = high_resolution_clock::now();
for (int i = 0; i < n; i++) { recursive(root_node); }
auto stop = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(stop - start);
cout << "recursion:" << duration.count() << endl;
start = high_resolution_clock::now();
for (int i = 0; i < n; i++) {
bool stop = false;
recursive2(root_node, &stop);
}
stop = high_resolution_clock::now();
duration = duration_cast<milliseconds>(stop - start);
cout << "recursion with early-break:" << duration.count() << endl;
start = high_resolution_clock::now();
for (int i = 0; i < n; i++) { loop(root_node); }
stop = high_resolution_clock::now();
duration = duration_cast<milliseconds>(stop - start);
cout << "loop:" << duration.count() << endl;
}
【问题讨论】:
-
与您正在运行的迭代次数相比,您正在遍历的树是如此之小,以至于管理堆栈对象的动态内存会使您从循环中获得的任何收益相形见绌。 (起草完整的答案)
-
感谢您的评论。在我的代码中,我通常处理比给定示例深 2-3 倍的树。性能特征与发布的相似。这是否意味着除非我有非常深的树,否则递归在我的情况下总是会更好?另外,是不是因为树的深度较浅,我也没有从早期休息中受益?
-
不一定。这只是意味着您需要更有效地分配动态内存。一个容易实现的结果是:默认情况下
std::stack<>使用std::deque进行存储,这在某些编译器上的实现速度非常慢。切换到 std::stack(int, std::vector` 可能会改变世界。对于小树,静态缓冲区会更好。 -
重要的是要记住,关于算法和数据结构的大多数想法都是关于接近无穷大的数据大小。您的数据集基本上为零。此外,这非常不是你做微基准测试的方式。你会得到垃圾结果。使用quickbench.com 和适当的内存修改技术来获得真正的结果。您确定您的循环甚至正在执行任何代码,而不仅仅是因为没有可见的输出而被省略?
-
@user3134575 如果你的真实代码也使用 arena-allocated 节点,那么通过在 arena 本身上循环而不是在树上循环访问每个节点将是尽可能高效。即只做
for(auto& node : nodes) {if (node.left == -1) {node.count++;}}
标签: c++ performance recursion tree depth-first-search