分支的影响

首先本文讲的是一个很浅显的道理,如果觉得简单不要拍我,之所以突然对分支预测感兴趣,是因为曾经看到有个商业的数据分析程序是这样写的:
If (version==0x001) && (vendor=abc) && (xxxx)
Do something
Else if (version=0x002) && (….)
Do something
Else if……

我曾经看过长达20多个if-else组成的判断,而随着新版本数据不断加入,if-else的组合还会越来越长。不仅要问,在一个mission-critical的数据分析程序里,开发人员可以这样毫无顾忌的挥霍资源吗?本文试着探索一些分支预测的奥秘。

用过GCC的人应该知道那个__builtin_expect的内建函数吧,那是GCC为了提高分支预测命中率而做的一种扩展。

正好昨天在看Dov Bulka的,其中也提到了避免分支预测能够提高程序的性能,

不禁好奇,到底分支预测会对程序性能有多大的影响。

就以最近遇到一个类似的问题为例:

在一个很高速的系统里,要反复处理下面的逻辑

方法1.
if (xxx)
func1() //func1可能可以被inline,或者直接在这里人为的展开
else (yyy)
func2()
else (zzz)
func3()

方法2.
直接用个数组保存func1-func3的函数指针,例如
array[xxx]=ptrOffunc1;
array[yyy]=ptrOffunc2;

到时直接调用array[xxx]()就可以了

哪一种方法更好?

在没进行测试之前,先基于固有的知识来研究这个问题。在指令流水线结构中,对于分支转移指令相当敏感。假设在80486的指令流水线中的第一条指令已进入到译码阶段,而第二条指令已进入到提取阶段(准备进入译码器),如果发现第一条指令是分支指令(如跳转到某个地址),则指令预取队列中下一条及下下条等指令预取无效。这时(确切地说,等到第一条指令执行期间形成了分支的目标地址),需从目标地址中现取指令,并交付执行,同时应立即清除指令预取队列,再将目标地址后面的指令预取过来填到队列中。这表明,一遇到分支指令,整个指令流水线就被打乱一次,稍后才能恢复到正常。显然,这影响了机器的运行速度。

Intel苦撑多年的netburst 架构无奈退出市场,就是因为超长的流水线造成设计过于复杂,首席执行官的惊天一跪虽然有点玩笑的意味,但也是Intel默认这种架构失败的反映。超长流水线为NetBurst带来了更高的频率,但同时也使得分支预测失败后流水线指令清空和重新加载所需要的周期大为延长。为此,现代处理器中使用了多种技术来降低这种影响,例如分支预测,乱序执行,分支延迟,分支目标缓冲器等等,然而,可以想象,如果分支过多,分支预测还是会不可避免的对程序性能造成一定的影响。

对于第1种方法,缺点就是分支的个数,如果分支过多而且不能保证命中率的话,在一个高速的系统里必然会带来程序的冗余;第二种方法避免了分支预测,但是又带来程序跳转,参数入栈的开销,相比之下第一种方法可以直接把函数inline或者人为的直接展开。考虑到函数inline的效果并不算很大(去除了函数跳转,参数压栈等指令),我估计最后会是CaseB胜出,不过,鹿死谁手,那真的需要测量结果来判断。

测试代码:
采用if-else跳转的caseA,一共11个分支

void BranchA2(int x)
{
if (x>RANGE*9/10)
{
HandleFuncA1(x);
}
else if (x>RANGE*8/10)
{
HandleFuncA2(x);
}
else if (x>RANGE*7/10)
{
HandleFuncA3(x);
}
else if (x>RANGE*6/10)
{
HandleFuncA4(x);
}
else if (x>RANGE*6/10)
{
HandleFuncA5(x);
}
else if (x>RANGE*5/10)
{
HandleFuncA6(x);
}
else if (x>RANGE*4/10)
{
HandleFuncA7(x);
}
else if (x>RANGE*3/10)
{
HandleFuncA8(x);
}
else if (x>RANGE*2/10)
{
HandleFuncA9(x);
}
else if (x>RANGE*1/10)
{
HandleFuncA10(x);
}
else
{
HandleFuncA11(x);
}

}
CaseA函数入口
void test_case4A()
{
printf("test case4A/n");
timespec l_startTime,l_endTime,l_interval;
int ret;
long long i_period;
clock_gettime(CLOCK_REALTIME,&l_startTime);
for (int i =0;i{
BranchA2(inp[i%3000]);
}
clock_gettime(CLOCK_REALTIME,&l_endTime);
ret = delta_t(&l_interval, &l_startTime, &l_endTime);
i_period = l_interval.tv_sec*1000000000+l_interval.tv_nsec;
printf("branchA time=%u/n",i_period);
}

采用函数指针数组跳转的caseB:
void BranchB(int x)
{
array[x](x);
}
void test_case4B()
{
printf("test case4B/n");
timespec l_startTime,l_endTime,l_interval;
int ret;
long long i_period;
clock_gettime(CLOCK_REALTIME,&l_startTime);
for (int i =0;i{
BranchB(inp[i%3000]);
}
clock_gettime(CLOCK_REALTIME,&l_endTime);
ret = delta_t(&l_interval, &l_startTime, &l_endTime);
i_period = l_interval.tv_sec*1000000000+l_interval.tv_nsec;
printf("branchB time=%u/n",i_period);
}
由于方法2采用的是函数跳转的方法,所以HandleFunc不可能被inline.

测试结果:
首先初始化一个随机数组:
srand ( time(NULL) );
for (int j = 0; j < 3000; j++) {
inp[j] = rand()%30;
}
初始化array[]函数指针数组:
array[0]=HandleFuncA0;
array[1]=HandleFuncA1;

array[29]=HandleFuncA29;
人为制造一些伪处理代码:
Int count=0;//全局变量
inline void HandleFuncA0(int y)
{ count=y+3;}
inline void HandleFuncA1(int y)
{ count=y++;}

之所以这样做是为了让编译器认为这些处理代码在真的做些事情,而不会把他们优化掉。
好了,准备就绪,编译:
g++ -g -O2 -o branchtest.o2 branchtest.cpp -lrt
在使用vtune测量前,使用-g –O2之类的开关进行编译,这样才能避免是由于gcc未对代码进行优化而导致程序的性能恶化。
在8 core XEON上的结果:
test case4A
branchA time=2510484704

test case4B
branchB time=1934092704

假如将BranchA2中的分支降低到3条的时候,从而case A-1的时候结果为:
test case4A-1
branchA time=921836704

再看看vtune的sample结果,CaseA占用全部分支预测失败的52.82%, case A-1占23.88%,而caseB只占据8.87%,再结合CPU_CLK_UNH的结果来看,CaseA也占据了绝大部分比例,因此,可以判断CaseA是一个程序执行得hotspot。而intel的软件优化手册上也提到,首先判断分支预测的失败率,一般5%以上值得优化,然后还需要结合CPI来看,这个函数是不是确实为执行热点,有时候也存在分支预测失败率很高,但并非执行热点的情况。

最后得出一个简单的结果,在分支比较多的时候,是用函数指针执行快,而在分支比较少的时候,if-else的判断方式快。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值