为什么我对这段代码的基于堆栈的实现比递归慢得多
Why is my stack-based implementation of this code so much slower than recursion?
我有一个树,它的节点存储-1或作为顶点名称的非负整数。每个顶点在树中最多出现一次。以下函数是我代码中的一个瓶颈:
版本A:
void node_vertex_members(node *A, vector<int> *vertexList){
if(A->contents != -1){
vertexList->push_back(A->contents);
}
else{
for(int i=0;i<A->children.size();i++){
node_vertex_members(A->children[i],vertexList);
}
}
}
版本B:
void node_vertex_members(node *A, vector<int> *vertexList){
stack<node*> q;
q.push(A);
while(!q.empty()){
int x = q.top()->contents;
if(x != -1){
vertexList->push_back(x);
q.pop();
}
else{
node *temp = q.top();
q.pop();
for(int i=temp->children.size()-1; i>=0; --i){
q.push(temp->children[i]);
}
}
}
}
出于某种原因,版本B的运行时间明显长于版本A,这是我没有预料到的。编译器可能在做什么比我的代码聪明得多?换句话说,我在做什么这么低效?同样让我困惑的是,如果我尝试任何事情,比如在将孩子们的内容放入堆栈之前检查版本B中的内容是否为-1,它的速度会显著减慢(几乎是3倍)。作为参考,我在Cygwin中使用g++和-O3选项。
更新:
我能够使用以下代码(版本C)匹配递归版本:
node *node_list[65536];
void node_vertex_members(node *A, vector<int> *vertex_list){
int top = 0;
node_list[top] = A;
while(top >= 0){
int x = node_list[top]->contents;
if(x != -1){
vertex_list->push_back(x);
--top;
}
else{
node* temp = node_list[top];
--top;
for(int i=temp->children.size()-1; i>=0; --i){
++top;
node_list[top] = temp->children[i];
}
}
}
}
明显的缺点是代码长度和幻数(以及相关的硬限制)。而且,正如我所说,这只符合版本A的性能。我当然会坚持递归版本,但我现在很满意,因为它基本上是STL开销。
版本A有一个显著的优势:代码大小要小得多。
版本B有一个显著的缺点:为堆栈元素分配内存。假设堆栈一开始是空的,并且有元素被一个接一个地推入其中。每隔一段时间,就必须为基础数据进行新的分配。这是一个昂贵的操作,每次调用函数时可能会重复几次。
编辑:这是g++ -O2 -S
在Mac OS上使用GCC 4.7.3生成的程序集,通过c++filt
运行,并由我注释:
versionA(node*, std::vector<int, std::allocator<int> >*):
LFB609:
pushq %r12
LCFI5:
movq %rsi, %r12
pushq %rbp
LCFI6:
movq %rdi, %rbp
pushq %rbx
LCFI7:
movl (%rdi), %eax
cmpl $-1, %eax ; if(A->contents != -1)
jne L36 ; vertexList->push_back(A->contents)
movq 8(%rdi), %rcx
xorl %r8d, %r8d
movl $1, %ebx
movq 16(%rdi), %rax
subq %rcx, %rax
sarq $3, %rax
testq %rax, %rax
jne L46 ; i < A->children.size()
jmp L35
L43: ; for(int i=0;i<A->children.size();i++)
movq %rdx, %rbx
L46:
movq (%rcx,%r8,8), %rdi
movq %r12, %rsi
call versionA(node*, std::vector<int, std::allocator<int> >*)
movq 8(%rbp), %rcx
leaq 1(%rbx), %rdx
movq 16(%rbp), %rax
movq %rbx, %r8
subq %rcx, %rax
sarq $3, %rax
cmpq %rbx, %rax
ja L43 ; continue
L35:
popq %rbx
LCFI8:
popq %rbp
LCFI9:
popq %r12
LCFI10:
ret
L36: ; vertexList->push_back(A->contents)
LCFI11:
movq 8(%rsi), %rsi
cmpq 16(%r12), %rsi ; vector::size == vector::capacity
je L39
testq %rsi, %rsi
je L40
movl %eax, (%rsi)
L40:
popq %rbx
LCFI12:
addq $4, %rsi
movq %rsi, 8(%r12)
popq %rbp
LCFI13:
popq %r12
LCFI14:
ret
L39: ; slow path for vector to expand capacity
LCFI15:
movq %rdi, %rdx
movq %r12, %rdi
call std::vector<int, std::allocator<int> >::_M_insert_aux(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&)
jmp L35
这相当简洁,乍一看似乎相当没有"减速带"。当我用-O3编译时,我会得到一个可怕的混乱,有展开的循环和其他有趣的东西。我现在没有时间对版本B进行注释,但可以说它更复杂,因为有很多deque函数,并且占用了更多的内存。毫不奇怪它慢了。
版本B中q
的最大大小明显大于版本A中的最大递归深度。这可能会降低缓存性能的效率。
(版本A:深度为log(N)/log(b)
,版本B:队列长度达到b*log(N)/log(b)
)
第二个代码速度较慢,因为除了要返回的集合之外,它还维护第二个动态集数据结构。这涉及到更多的内存分配、更多的对象初始化、更多的列表插入和删除。
然而,第二段代码中的算法更灵活:可以对其进行简单的修改,使其具有广度优先遍历而不是深度优先遍历,而递归只执行深度优先遍历。(好吧,它可以先深入研究,但变化并不是那么微不足道;请参阅最后的评论。)
由于任务是遍历所有内容并收集一些节点,所以假设您不想要深度优先顺序,那么深度优先遍历可能更好。
但是,在搜索满足某些条件的节点的情况下,实现广度优先搜索可能更合适。如果树是无限的(因为它不是一个数据结构,而是一个可能性的搜索树,比如游戏中的未来移动或其他什么),那么首先做深度可能是很难的,因为没有底部。在某些情况下,希望找到一个靠近根的节点,而不仅仅是任何节点。深度优先搜索可能需要很长时间才能找到靠近树根的节点。如果树很深,但通常在离根不远的地方找到所需的节点,那么深度优先搜索可能会浪费大量时间,即使实现它的递归机制很快。
递归可以通过迭代深化来实现广度优先:递归到最大深度1,然后再次从顶部递归,这次是最大深度2,依此类推。基于队列的遍历只需要改变向工作队列添加节点的顺序。
- 如何在 c++ 中实现堆栈数组?
- 使用链表实现堆栈时出错
- 在给定程序中降低矢量数组实现堆栈的时间复杂度有哪些不同的可能方法?
- C++ 使用数组实现堆栈
- 关于在C 中实现堆栈的问题
- 使用链接列表在C 中实现堆栈
- c++ 中 if 语句中的多个条件(通过链表实现堆栈)
- C 内存泄漏错误在实现堆栈类时
- 尝试实现堆栈时C++未定义的引用
- 链表与动态数组用于使用向量类实现堆栈
- 在C++中实现堆栈类
- 可视化 在C++中实现堆栈
- 使用链接列表实现堆栈,调试断言失败
- 我怎样才能实现堆栈的向量
- 在没有动态内存分配的情况下实现堆栈
- 如何使用 std::vector 实现堆栈
- 在c++中使用链表实现堆栈
- 在哪里实现堆栈类(在非递归二进制搜索函数中使用)
- 使用双链表实现堆栈的错误
- 在c++中使用链表实现堆栈,复制构造函数