Branchless性能优化编程

Branchless可以优化多少性能

例子1:

f (bool b, unsigned long x, unsigned long& s) {
    if (b) s += x;
}

优化前:130M calls/second

优化后:400M calls/second

例子二:

if (x[i] || y[i]) {
 ...
}

优化前:150M calls/second

优化后:570M calls/second

理解Branchless前,需要准备一部分体系结构的知识。

体系结构

branchless-opt

体系结构优化包括SSE并行优化/cache优化/pipeline优化/分支预测优化/循环优化等。

这些很多都是由编译器完成了。

但这不代表程序员不需要去关注这些

因为编译器远没有我们想象的聪明。

当程序员理解了这些技术后,可以在一些关键问题上,给出一般人给不出来的解决方案

计算资源问题思考

例子实验

考虑以下程序(code 1.1):

unsigned long v1[N], v2[N]; 
unsigned long a = 0;
for (size_t i = 0; i < N; ++i) {
   a += v1[i]*v2[i]; 
}

在CPU中执行过程如下:

执行 a += v1[i]*v2[i]; 
Init:
              register: i  |
---------------------------------------------------------
register: v1               | memory: v1[i]
register: v2               | memory: v2[i]
register: a1               |
                       CPU | Memory 
                       
Step 1: load v1
              register: i  |
---------------------------------------------------------
register: v1 <- read: v1[i]| memory: v1[i]
register: v2               | memory: v2[i]
register: a1               |
                       CPU | Memory 

Step 2: load v2
              register: i  |
---------------------------------------------------------
register: v1               | memory: v1[i]
register: v2 <- read: v2[i]| memory: v2[i]
register: a1               |
                       CPU | Memory 

Step 3 mul
              register: i  |
---------------------------------------------------------
register: v1 -------       | memory: v1[i]
register: v2 ------ |      | memory: v2[i]
                multipy     |
register: a1     <- |      |
                       CPU | Memory 

再考虑以下程序(code 1.2):

unsigned long v1[N], v2[N]; 
unsigned long a1 = 0;
unsigned long a2 = 0;
for (size_t i = 0; i < N; ++i) {
    a1 += v1[i]*v2[i]; 
    a2 += v1[i]+v2[i]; 
}

比较代码 code 1.1与code 1.2的benchmark发现

两者并没有明显的差别。

结论

这是因为CPU的寄存器有非常多个

多余的计算资源可以同时完成更多的指令

当计算与计算是各自独立的,那么可以很好的利用多余的计算资源,并行完成多个计算

但实际情况会复杂很多

当前指令的数据可能依赖上一条指令的输出

当前指令的执行可能依赖上一条指令的条件判断

cpu pipeline 的计算过程

例子

code 2.1

for (int i=0; i

单条语句的执行过程:

------------------------time line---------------------------
 exec s[i]:v1[i]+v2[i]  |  exec  s[i]*d[i] (data dependency)
 exec d[i]:v1[i]-v2[i]  |

语句s[i]*d[i]的执行以来于前两条语句执行的结果,所以这三条语句,只有前两条可以并行执行,最后一条语句则需要等待前两条语句都完成后,才可以执行

循环展开优化

code 2.2

for (int i=0; i

通过展开循环体

就可以通过多pipeline同时执行这四条语句了

过程如下:

------------------------time line---------------------------
 exec s[i]:v1[i]+v2[i]        |  exec  s[i]*d[i] 
 exec d[i]:v1[i]-v2[i]        |
 exec s[i+1]:v1[i+1]+v2[i+1]  |  exec  s[i+1]*d[i+1] 
 exec d[i+1]:v1[i+1]-v2[i+1]  |
 exec s[i+2]:v1[i+2]+v2[i+2]  |  exec  s[i+2]*d[i+2] 
 exec d[i+2]:v1[i+2]-v2[i+2]  |
 exec s[i+3]:v1[i+3]+v2[i+3]  |  exec  s[i+3]*d[i+3] 
 exec d[i+4]:v1[i+4]-v2[i+4]  |

条件分支与cpu pipeline

例子

code 3.1

for (int i=0; iv2[i])?v1[i]:v2[i];
}

分析

pipeline对于独立的语句,可以很好的优化执行

但是对于上面这个例子code 3.1

下一条执行的语句,依赖上一条判断语句(v1[i]>v2[i])的真假来决定

cpu执行一条语句是不能在一个clock内执行完的

一般需要4~5个clock才能执行完一条语句

那么代表一下条语句必须让cpu停顿若干个clock,才能执行下一条语句

对于循环体的 i

但是对于v1[i] > v2[i],情况就复杂了。

分支预测

分支预测是CPU的一个单元对条件判断语句的结果进行预测的行为

它通过条件判断语句几次的结果输出,进行模式预测

如果预测对了,那么什么也不会发生

如果预测错了.....

分支预测错误会发生什么

  • 所有预测后的计算需要被废弃
  • 新的分支的计算需要开始
  • 所有因预测错误产生的内存结果需要废弃
  • 所有因预测错误产生的异常需要废弃
思考下面的例子

code 4.1

if (p != NULL) 
    *p = 1; // p is rarely NULL int v[N];
for (size_t i=0; i
分析

p大部分时间都是非空

cpu所以一直预测p不为空

那么*p=1的执行则会发生异常

但是当cpu执行完条件语句p != NULL,发现输出为false

那么*p=1的执行发生的异常就需要被废弃掉

同样的

对于i

当i=N时, cpu也预测为真

那么v[N] = N就会被执行

当i

那么就需要回退v[N] = N的写内存的结果

通过例子来思考分支预测

可预测与不可预测性能比较

例子

branchless_predit

上面这两个例子,

左边这个的分支一直输出true,是可以被cpu预测准确的

右边这个的分支输出是随机的,是不可以背哦cpu准确预测的。

下面我们看看他们的性能结果:

不可预测的结果:

branchless_predit_result_1

可以看到分支预测错误占了10%

可预测的结果:

branchless_predit_result_2

可以看到分支miss降低到0.06%

吞吐量也涨了几倍

结论

这代表cpu对于特定模式,是可以做出正确的预判的

分支优化在通常情况下,是不必的

因为cpu的预测可以预测的很好

可预测与不可预测性能比较(更复杂的例子)

例子

branchless_predit_1

左边的代码代表不可预测

右边的代码是true和false交错出现的

相同的,我们来看看性能结果

不可预测的结果:

branchless_predit_result_1

可以看到分支预测错误占了10%

可预测的结果

branchless_predit_result_3

cpu可以正确的识别这种复杂的场景,并正确的预判了程序的行为

结论

不要在一开始就进行分支优化

需要知道等到用perf确认后,才进行必要的分支优化

多分支预测

if (x || y)
    do_it(); 
else
    dont_do_it();

从程序员的视觉:如果 x||y是经常是true/false的,那么cpu应该可以预测正确

cpu的视觉:如果x经常是true/false,那么第一个分支是可预测的;如果x为false,y经常为true/false,那么第二个分支是可预测的

那这就有问题了,因为x是不可预测的,y是不可预测的,即使x和y结合后,输出的稳定的,cpu并没有那么强大可以识别出来

例子

branchless_predit_2

结果

branchless_predit_result_4

可以看到,分支预测并不能很好的处理这种情况

优化

if (x || y) 

=>

if (bool(x) + bool(y))

通过把bool累加后,转换为单条件语句

有些编译器会做上面的优化,有些不会,所以优化前,需要先确认,再进行优化

优化后的例子

branchless_predit_3

结果

branchless_predit_result_5

cpu成功的做出了预测

branchless优化技巧

技巧1

sum += cond ? expr1 : expr2;
=转化=>
term[2] = { expr2, expr1 }; 
sum += term[bool(cond)];

例子

branchless_predit_4

左边是一个不可预测的例子

右边是通过去掉分支,优化的例子

结果

branchless_predit_result_6

跟正确预测的版本对比,发现性能稍微下降了一点

但是跟不可预测的版本对比,branchless的性能是提高了几倍

结论

branchless优化是通过让cpu做更多的计算来减少分支代码的

技巧2

if (b) {
    s += x;
}
==>
s += b*x;

例子:

branchless_predit_5

结果

branchless_predit_result_7

结论

优化后,性能提高了3倍

总结

分支优化前,先验证,因为编译器和cpu分支预测已经做的很好

确认发生分支miss太多后,才去进行branchless优化

branchless的代价是让cpu做更多的计算来减少分支预测失效

你可能感兴趣的:(Branchless性能优化编程)