前言:
这一段时间遇上了烦心事,差不多一个月没有更新博客和github了,打算在这周之内把拉下的内容都补上吧。
这次要写的内容,是我在实验室开发的项目中遇到的问题,主要是关于性能优化。实验室的项目代码写完之后,发现运行的速度实在是太慢了,于是需要找到性能的瓶颈,在第二版程序上修复这个问题。
我的github:
我实现的代码全部贴在我的github中,欢迎大家去参观(这一部分内容就没有github的代码了,毕竟是实验室的内容)。
https://github.com/YinWenAtBIT
性能测试:
性能测试方法:
在这里要测试性能就必须使用profile工具,由于是在vs2010上开发的,自然使用它提供的工具最为方便。
一、Visual Studio 2010 Profile工具:
1. 测试最耗时的代码:
这里代码是有许多的模块构成,得到各个模块的耗时之后,还需要深入模块内部,找到模块内真正耗时的地方。
先进入最耗时的模块:
这个模块里func_ADBF耗时最多,查看到底是什么样的问题
这三条语句暂用了总时间的63%,他们语句做的是同一件事情,从Eigen矩阵库矩阵中取j行,i+9列的std::complex<double>复数,用来与ctrPara中保存的复数相乘,实际上这个循环是通过叠加的方式实现了三次矩阵乘法。
我们再去看看耗时34.8%的模块中怎么花的时间。可以看到,在另一个模块中也是复数乘法占用了大量的时间。另外,就是存储在EIgen矩阵中的提取数据或者保存数据占用了大量时间
挖掘提高性能方法:
一、提高性能可以使用的方法:
由上一节可知,出现性能瓶颈的地方在于Eigen矩阵中的寻址操作以及复数的乘法与加法。
并且现在性能瓶颈出现在两个模块中,我们可以尝试在第一个模块中修改代码,尝试提高性能,第二个模块保持不变,这样就能明显的观察出性能是否有明显的提升。
二、减少重复的寻址操作:
原有的代码中,我们可以看到,对于TargetEcho(j,i+9)这个地址的数据进行了三次寻址,如果这个地方确实花费时间,那么经过这个修改,应该能有明显的改善:
新代码运行时间对比:
结果有了明显的改善,如果我们假设原来第二个模块的运行时长是35,第一个模块时长64。那么现在第二个模块还是35,第一个模块的时长已经变成了40.3。显然,这一步是一个明显的改善,确实在Eigen矩阵中寻址花费了很长的时间。
进入到这个函数,发现除了寻址占用16%的时间以外,复数的乘法与加法也占用了不少的时间,所得到的性能提升还是太小。
对于Eigen矩阵寻址耗时时间太长,我们可以想办法尝试降低寻址的开销
三、利用局部性优化:
上面的代码可以看到,在内部循环中,每次对Eigen的寻址,是增加的行数,那么对于每次的调用,没两个数据之间要差上2048个数,加上两个double来保存复数,那么每次移动的数据大小至少16kb,是无法保存在高速缓存L1中的,尝试修改矩阵的循环顺序来达到提高效率。
修改方式:矩阵乘法由ijk运算模式变成kij运算:
结果如下:
提升了0.05%,这个数据微乎其微,还不能确定是否是随机的结果,看来这个局部性的开销在整个的Eigen寻址中的开销占比太小,不值得花时间去提高了。那么这样的话对于Eigen矩阵来说,没有多少可以优化的地方了,需要替换Eigen,使用新的更高效寻址的数据结构来解决这个性能问题。因此,暂且假设这个部分已经优化,把关注点放到其他的地方。
四、尝试寻找STL复数类瓶颈:
这两个部分已经在《UNIX网络编程》中已经有了详细的叙述了,因此略过。
我们首先寻找STL复数的乘法与加法操作各自占用了多长的时间,然后再针对性的优化。首先我们把乘法与加法独立出来,结果如下:
可以见到,乘法与加法用时差不多,但是感觉这似乎不符合常理,因为毕竟复数的加法只需要实部与实部相加,虚部与虚部相加,不应该占用这么长的时间。我们尝试把乘法的实部和虚部提取出来相加,最后再合起来赋值。
新代码如下:
我们把求和分别放在了各自的Real和Imag的double变量中,最后再组成复数。
结果让人震惊:
模块1的时间下降了大约1/2。根据比例关系,原来模块2占时35,比例为34%,现在时间未变,比例变成51%,意味着模块1的占时已经变成了33(最初为64)。看来官方的STL运算速度比起我们想象的要慢多了。
五、尝试手动计算复数乘法:对于上一部分的尝试,发现复数的加法的开销似乎比我预想的要大多了。那么是否复数的乘法也有过大的开销呢,我于是决定把乘法部分也自行重载计算,原有的std::complex<double>只用来保存数据。
新的代码如下:
新的代码运行速度如下:
显然,运行速度再次提升,该模块的运行时间已经下降到25.5(原来是64),意味着这一部分确实是性能的瓶颈。
从单次复数乘法与加法上的耗时来看,乘法与加法耗时不多,反而是取出复数的实部与虚部最花费时间,看来是非常有必要找一个STL的代替品来执行该运算了。
测试总结:
1. Eigen矩阵的寻址操作,需要考虑通过别的数据结构来实现数据储存。
2. STL的复数加法与乘法速度太慢,考虑寻找更优的复数库来提升效率
于是接下来的工作,需要在添加新的功能之外,开发自己使用的矩阵类已经高性能的复数类。