【发布时间】:2017-01-23 15:40:17
【问题描述】:
设A 是一个包含奇数个零和一的数组。如果n 是A 的大小,则构造A 使得第一个ceil(n/2) 元素为0,其余元素为1。
所以如果n = 9,A 看起来像这样:
0,0,0,0,0,1,1,1,1
我们的目标是在数组中找到1s 的总和,我们使用这个函数来做到这一点:
s = 0;
void test1(int curIndex){
//A is 0,0,0,...,0,1,1,1,1,1...,1
if(curIndex == ceil(n/2)) return;
if(A[curIndex] == 1) return;
test1(curIndex+1);
test1(size-curIndex-1);
s += A[curIndex+1] + A[size-curIndex-1];
}
对于给定的问题,这个函数相当愚蠢,但它是一个不同函数的模拟,我希望看起来像这样并且产生相同数量的分支错误预测。
下面是整个实验的代码:
#include <iostream>
#include <fstream>
using namespace std;
int size;
int *A;
int half;
int s;
void test1(int curIndex){
//A is 0,0,0,...,0,1,1,1,1,1...,1
if(curIndex == half) return;
if(A[curIndex] == 1) return;
test1(curIndex+1);
test1(size - curIndex - 1);
s += A[curIndex+1] + A[size-curIndex-1];
}
int main(int argc, char* argv[]){
size = atoi(argv[1]);
if(argc!=2){
cout<<"type ./executable size{odd integer}"<<endl;
return 1;
}
if(size%2!=1){
cout<<"size must be an odd number"<<endl;
return 1;
}
A = new int[size];
half = size/2;
int i;
for(i=0;i<=half;i++){
A[i] = 0;
}
for(i=half+1;i<size;i++){
A[i] = 1;
}
for(i=0;i<100;i++) {
test1(0);
}
cout<<s<<endl;
return 0;
}
输入g++ -O3 -std=c++11 file.cpp编译,输入./executable size{odd integer}运行。
我正在使用 Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz,8 GB RAM,L1 缓存 256 KB,L2 缓存 1 MB,L3 缓存 6 MB。
运行 perf stat -B -e branches,branch-misses ./cachetests 111111 给了我以下信息:
Performance counter stats for './cachetests 111111':
32,639,932 branches
1,404,836 branch-misses # 4.30% of all branches
0.060349641 seconds time elapsed
如果我删除线
s += A[curIndex+1] + A[size-curIndex-1];
我从 perf 得到以下输出:
Performance counter stats for './cachetests 111111':
24,079,109 branches
39,078 branch-misses # 0.16% of all branches
0.027679521 seconds time elapsed
当它甚至不是 if 语句时,该行与分支预测有什么关系?
在我看来,在test1() 的第一个ceil(n/2) - 1 调用中,两个 if 语句都是错误的。在ceil(n/2)-th 调用中,if(curIndex == ceil(n/2)) 将为真。在剩余的n-ceil(n/2) 调用中,第一条语句为假,第二条语句为真。
为什么英特尔无法预测如此简单的行为?
现在让我们看看第二种情况。假设A 现在有交替的零和一。我们总是从 0 开始。所以如果 n = 9 A 看起来像这样:
0,1,0,1,0,1,0,1,0
我们要使用的函数如下:
void test2(int curIndex){
//A is 0,1,0,1,0,1,0,1,....
if(curIndex == size-1) return;
if(A[curIndex] == 1) return;
test2(curIndex+1);
test2(curIndex+2);
s += A[curIndex+1] + A[curIndex+2];
}
这里是整个实验的代码:
#include <iostream>
#include <fstream>
using namespace std;
int size;
int *A;
int s;
void test2(int curIndex){
//A is 0,1,0,1,0,1,0,1,....
if(curIndex == size-1) return;
if(A[curIndex] == 1) return;
test2(curIndex+1);
test2(curIndex+2);
s += A[curIndex+1] + A[curIndex+2];
}
int main(int argc, char* argv[]){
size = atoi(argv[1]);
if(argc!=2){
cout<<"type ./executable size{odd integer}"<<endl;
return 1;
}
if(size%2!=1){
cout<<"size must be an odd number"<<endl;
return 1;
}
A = new int[size];
int i;
for(i=0;i<size;i++){
if(i%2==0){
A[i] = false;
}
else{
A[i] = true;
}
}
for(i=0;i<100;i++) {
test2(0);
}
cout<<s<<endl;
return 0;
}
我使用与以前相同的命令运行 perf:
Performance counter stats for './cachetests2 111111':
28,560,183 branches
54,204 branch-misses # 0.19% of all branches
0.037134196 seconds time elapsed
删除该行再次改善了一些情况:
Performance counter stats for './cachetests2 111111':
28,419,557 branches
16,636 branch-misses # 0.06% of all branches
0.009977772 seconds time elapsed
现在如果我们分析函数,if(curIndex == size-1) 将是 false n-1 次,if(A[curIndex] == 1) 将在 true 和 false 之间交替。
在我看来,这两个函数都应该很容易预测,但第一个函数并非如此。同时,我不确定那条线发生了什么以及为什么它在改善分支行为方面发挥作用。
【问题讨论】:
-
你确定这是对的吗?我看到双重递归最终会遍历数组两次
-
不同的汇编代码是什么样的?
-
在第一个函数中,如果
curIndex没有指向最后一个0并且也没有指向1,我们递增curIndex。如果数组是从0索引的,倒数第二个0将位于(floor(n/2) - 1)位置,我们将进行的最高跳跃将指向n-(floor(n/2) - 1)-1 = n - floor(n/2),它应该指向最后一个0之后的元素.如果我们在位置0,我们将跳转到(n-0-1),它将指向数组中的最后一个元素。至于第二个函数,我们也是这样做的,当我们到达最后一个0时,索引将等于n-1,所以我们将停止。 -
@jsguy 可惜还没人回答。我建议添加performance 标签,后面有很多标签,因此可能会吸引一些错过这个问题的人。我自己已经提出了这个修改,但被拒绝了。我不想再提交了,我把它留在这里作为给你的建议。您的来电。
-
你用cachegrind看了吗? (valgrind.org/docs/manual/cg-manual.html)
标签: c++ performance branch-prediction