C++ 为什么DFS在一棵树中较慢,而在另一棵树中较快?
更新:原来生成树的解析器中有一个bug。更多信息请参见最终编辑。 设C++ 为什么DFS在一棵树中较慢,而在另一棵树中较快?,c++,algorithm,performance,caching,tree,C++,Algorithm,Performance,Caching,Tree,更新:原来生成树的解析器中有一个bug。更多信息请参见最终编辑。 设T为二叉树,使每个内部节点正好有两个子节点。对于这棵树,我们要编写一个函数,用于为T中的每个节点v查找由v定义的子树中的节点数 示例 输入 期望输出 红色表示我们要计算的数字。树的节点将存储在一个数组中,让我们按照预排序布局将其称为treearlay 对于上面的示例,trearray将包含以下对象: 10,11,0,12,13,2,7,3,14,1,15,16,4,8,17,18,5,9,6 树的节点由以下结构描述: str
T
为二叉树,使每个内部节点正好有两个子节点。对于这棵树,我们要编写一个函数,用于为T
中的每个节点v
查找由v
定义的子树中的节点数
示例
输入
期望输出
红色表示我们要计算的数字。树的节点将存储在一个数组中,让我们按照预排序布局将其称为treearlay
对于上面的示例,trearray
将包含以下对象:
10,11,0,12,13,2,7,3,14,1,15,16,4,8,17,18,5,9,6
树的节点由以下结构描述:
struct tree_node{
long long int id; //id of the node, randomly generated
int numChildren; //number of children, it is 2 but for the leafs it's 0
int size; //size of the subtree rooted at the current node,
// what we want to compute
int pos; //position in TreeArray where the node is stored
int lpos; //position of the left child
int rpos; //position of the right child
tree_node(){
id = -1;
size = 1;
pos = lpos = rpos = -1;
numChildren = 0;
}
};
用于计算所有大小
值的函数如下所示:
void testCache(int cur){
if(treeArray[cur].numChildren == 0){
treeArray[cur].size = 1;
return;
}
testCache(treeArray[cur].lpos);
testCache(treeArray[cur].rpos);
treeArray[cur].size = treeArray[treeArray[cur].lpos].size +
treeArray[treeArray[cur].rpos].size + 1;
}
我想了解为什么当T
看起来像这样(几乎像一条左行链)时,此函数会更快:
当T
看起来像这样时(几乎像一条右转的链条)会变慢:
以下实验在Intel(R)Core(TM)i5-3470 CPU上运行,运行频率为3.20GHz,内存为8GB,一级缓存256KB,二级缓存1MB,三级缓存6MB
图中的每个点都是以下for循环的结果(参数由轴定义):
通过键入g++-O3-std=c++11 file.cpp编译
通过键入/executable tree.txt
运行。在tree.txt
中,我们将树存储在
是一棵左行的树,有10^5片叶子
是一棵有10^5片叶子的树
我得到的跑步时间:
左行树木约0.07秒
约0.12秒用于正确行驶的树木
很抱歉我写了这么长的帖子,但是考虑到问题的范围似乎很窄,我找不到更好的方式来描述它
提前谢谢你
编辑:
这是史密斯先生回答后的后续编辑。我知道地方扮演着非常重要的角色,但我不确定我是否理解这里的情况
对于上面的两个示例树,让我们看看如何随时间访问内存
对于左边的树:
对于正确的路径树:
在我看来,在这两种情况下,我们都有本地访问模式
编辑:
以下是关于条件分支数量的曲线图:
以下是有关分支预测失误数量的图表:
是一棵左行的树,有10^6片叶子
是一棵有10^6片叶子的树
最终编辑:
我想为浪费大家的时间而道歉,我使用的解析器有一个参数,表示我希望使我的树看起来像“左”或“右”。这是一个浮点数,必须接近0才能向左移动,接近1才能向右移动。然而,要使它看起来像一条链,它必须非常小,比如0.000000001
或0.99999999
。对于较小的输入,即使对于0.0001
这样的值,树看起来也像一条链。我认为这个数字足够小,它也会为较大的树提供一个链,然而,正如我将要展示的那样,情况并非如此。如果使用像0.00000000 1
这样的数字,解析器将由于浮点问题而停止工作
瓦迪克罗博特的回答表明我们有地方问题。受他的实验启发,我决定对上面的访问模式图进行概括,以了解它不仅在示例树中,而且在任何树中的行为
我修改了vadikrobot的代码如下:
void testCache(int cur, FILE *f) {
if(treeArray[cur].numChildren == 0){
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", cur);
treeArray[cur].size = 1;
return;
}
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", cur);
testCache(treeArray[cur].lpos, f);
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", cur);
testCache(treeArray[cur].rpos, f);
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", cur);
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", treeArray[cur].lpos);
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", treeArray[cur].rpos);
treeArray[cur].size = treeArray[treeArray[cur].lpos].size +
treeArray[treeArray[cur].rpos].size + 1;
}
由错误的解析器生成的访问模式
让我们看看左边一棵有10片叶子的树
看起来很不错,正如上面的图表中所预测的(我只是在上面的图表中忘记了这样一个事实:当我们找到一个节点的大小时,我们还访问该节点的大小参数,cur
,在上面的源代码中)
让我们看看左边一棵有100片叶子的树
看起来和预期的一样。1000片树叶怎么样
这绝对不是意料之中的。右上角有一个小三角形。这是因为这棵树看起来不像是一条左行的链条,在最后的某个地方有一个小的子树。当叶数为10^4时,问题变得更大
让我们看看正确的树会发生什么。当叶数为10时:
看起来不错,100片叶子怎么样
看起来也不错。这就是为什么我质疑正确树木的位置,对我来说,它们至少在理论上都是局部的。现在,如果您尝试增大尺寸,会发生一些有趣的事情:
对于1000页:
对于10^4页,事情变得更加混乱:
由正确的解析器生成的访问模式
我没有使用通用解析器,而是为这个特定问题创建了一个解析器:
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char* argv[]){
if(argc!=4){
cout<<"type ./executable n{number of leafs} type{l:left going, r:right going} outputFile"<<endl;
return 0;
}
int i;
int n = atoi(argv[1]);
if(n <= 2){cout<<"leafs must be at least 3"<<endl; return 0;}
char c = argv[2][0];
ofstream fout;
fout.open(argv[3], ios_base::out);
if(c == 'r'){
for(i=0;i<n-1;i++){
fout<<"("<<i<<",";
}
fout<<i;
for(i=0;i<n;i++){
fout<<")";
}
fout<<";"<<endl;
}
else{
for(i=0;i<n-1;i++){
fout<<"(";
}
fout<<1<<","<<n<<")";
for(i=n-1;i>1;i--){
fout<<","<<i<<")";
}
fout<<";"<<endl;
}
fout.close();
return 0;
}
#包括
#包括
使用名称空间std;
int main(int argc,char*argv[]){
如果(argc!=4){
cout由于节点在内存中的位置不同,缓存未命中的情况也不同。如果您按照节点在内存中的顺序访问节点,则很可能缓存已经从缓存中的ram中加载了它们(因为加载缓存页(很可能比您的一个节点大)
如果您以随机顺序(相对于RAM中的位置)或相反顺序访问节点,则更可能是缓存尚未从RAM加载节点
因此,区别不是因为树的结构,而是树节点在RAM中的位置与您想要访问它们的顺序相比
编辑:(将访问模式添加到问题后):
正如您在访问模式gra上看到的那样
void testCache(int cur, FILE *f) {
if(treeArray[cur].numChildren == 0){
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", cur);
treeArray[cur].size = 1;
return;
}
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", cur);
testCache(treeArray[cur].lpos, f);
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", cur);
testCache(treeArray[cur].rpos, f);
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", cur);
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", treeArray[cur].lpos);
fprintf(f, "%d\t", tim++);
fprintf (f, "%d\n", treeArray[cur].rpos);
treeArray[cur].size = treeArray[treeArray[cur].lpos].size +
treeArray[treeArray[cur].rpos].size + 1;
}
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char* argv[]){
if(argc!=4){
cout<<"type ./executable n{number of leafs} type{l:left going, r:right going} outputFile"<<endl;
return 0;
}
int i;
int n = atoi(argv[1]);
if(n <= 2){cout<<"leafs must be at least 3"<<endl; return 0;}
char c = argv[2][0];
ofstream fout;
fout.open(argv[3], ios_base::out);
if(c == 'r'){
for(i=0;i<n-1;i++){
fout<<"("<<i<<",";
}
fout<<i;
for(i=0;i<n;i++){
fout<<")";
}
fout<<";"<<endl;
}
else{
for(i=0;i<n-1;i++){
fout<<"(";
}
fout<<1<<","<<n<<")";
for(i=n-1;i>1;i--){
fout<<","<<i<<")";
}
fout<<";"<<endl;
}
fout.close();
return 0;
}
void testCache(int cur, FILE *f) {
if(treeArray[cur].numChildren == 0){
fprintf (f, "%d\n", cur);
treeArray[cur].size = 1;
return;
}
fprintf (f, "%d\n", cur);
testCache(treeArray[cur].lpos, f);
fprintf (f, "%d\n", cur);
testCache(treeArray[cur].rpos, f);
fprintf (f, "%d\n", treeArray[cur].lpos);
fprintf (f, "%d\n", treeArray[cur].rpos);
treeArray[cur].size = treeArray[treeArray[cur].lpos].size + treeArray[treeArray[cur].rpos].size + 1;
}
valgrind --tool=callgrind --cache-sim ./a.out right
==11493== I refs: 427,444,674
==11493== I1 misses: 2,288
==11493== LLi misses: 2,068
==11493== I1 miss rate: 0.00%
==11493== LLi miss rate: 0.00%
==11493==
==11493== D refs: 213,159,341 (144,095,416 rd + 69,063,925 wr)
==11493== D1 misses: 15,401,346 ( 12,737,497 rd + 2,663,849 wr)
==11493== LLd misses: 329,337 ( 7,935 rd + 321,402 wr)
==11493== D1 miss rate: 7.2% ( 8.8% + 3.9% )
==11493== LLd miss rate: 0.2% ( 0.0% + 0.5% )
==11493==
==11493== LL refs: 15,403,634 ( 12,739,785 rd + 2,663,849 wr)
==11493== LL misses: 331,405 ( 10,003 rd + 321,402 wr)
==11493== LL miss rate: 0.1% ( 0.0% + 0.5% )
valgrind --tool=callgrind --cache-sim=yes ./a.out left
==11496== I refs: 418,204,722
==11496== I1 misses: 2,327
==11496== LLi misses: 2,099
==11496== I1 miss rate: 0.00%
==11496== LLi miss rate: 0.00%
==11496==
==11496== D refs: 204,114,971 (135,076,947 rd + 69,038,024 wr)
==11496== D1 misses: 19,470,268 ( 12,661,123 rd + 6,809,145 wr)
==11496== LLd misses: 306,948 ( 7,935 rd + 299,013 wr)
==11496== D1 miss rate: 9.5% ( 9.4% + 9.9% )
==11496== LLd miss rate: 0.2% ( 0.0% + 0.4% )
==11496==
==11496== LL refs: 19,472,595 ( 12,663,450 rd + 6,809,145 wr)
==11496== LL misses: 309,047 ( 10,034 rd + 299,013 wr)
==11496== LL miss rate: 0.0% ( 0.0% + 0.4% )