移动端arm cpu优化学习笔记----一步步优化盒子滤波(Box Filter)

       最近一段时间做比较多移动端开发相关的工作,感觉移动端优化相关的对我来说挺有趣的,以前都是在PC上写代码,写代码的时候对于代码的性能没有过多的思考和感觉。但是在移动端上写代码明显能察觉到一段代码写的好不好,对于在移动端上运行性能有很大的影响,尤其在一些比较老旧的机型上测试更能有感觉。

        然后最近刚好在复现一篇论文,要在MXNet中实现类似盒子滤波(box filter)的操作子,其实就是步长为1的sum pooling,盒子滤波算是很基础和经典的函数,但是在PC上实现的话因为有GPU,借助其强大的算力所以可以很暴力的实现,每个thread计算以某点为中心给定半径下的区域大小的和即可。然后突发奇想想试试在移动端cpu上试试如何写高效的盒子滤波操作。

        这篇文章就是把我的实践过程记录下来,首先给出最简单的实现然后如何一步步优化,到最后给出一个性能优化还不错的版本。由于我正式接触移动端优化的时间不长,很多东西理解的不深,所以有哪里论述不正确的地方请读者指出。

本文的代码:

boxFilter

1.Boxfilter最简单实现

        首先来看下Boxfilter最简单最直观的实现:


          对每个点,计算给定半径下区域的和,需要注意下边界的处理。其时间复杂度是 O( height x width x  (radius x 2 + 1) x (radius x 2 + 1) ),这个最简单的实现在输入大小固定的情况下,半径越大耗时越大,有很多重复计算的地方,相邻元素在计算各自区域内和的时候其实是有重叠的。然后第一个优化的思路就是boxfilter的计算是行列可分离的,具体可参考[3]。

2.Boxfilter优化第一版

      所谓行列可分离就是,把行列分开计算,从代码里可以看到,对每个元素,首先计算行方向上半径内的和,然后再计算列半径内的和,所以这时候的时间复杂度是 O(height x width x (radius x 2 + 1) x 2)。可以看到行列分离之后,时间复杂度减少了不少,尤其半径越大减少的越多,但是还是有重复计算的地方,而且在固定输入下时间复杂度还是会随半径的变大而变大。那么有没有方法可以使得计算复杂度不受半径的影响呢,这个优化思路就是比如在算某一行每个点的半径区域内的和时,对于行开头第一个点,首先计算其半径内和,然后对于接下来的点,不需要重新计算其半径区域内和,而是只需要把前一个元素半径内的和,按半径窗口偏移之后减去旧的点和加上新加入的点即可。

3.Boxfilter优化第二版


 这一版时间复杂度大概是O(height x width x 4 ),不算边界只看中间部分的计算就是一次加法和一次减法,行方向和列方向都一样。这里行方向的部分很好理解,因为边界部分需要特殊处理,比如开始部分只有加,结尾部分只有减法,所以计算分成了3部分。列方向计算的话按照常规思路,那就是按一列列来处理,可是我们知道数据一般是按照行来存储的,这样子跳行取数据,会造成很多次cache miss,这样子性能肯定会受很大的影响,所以这里用了一个大小是width的向量colSum,来存储每一列对应点的半径区域内的和,然后遍历的时候还是按照行来遍历,如果一下子理解不了这个思路的话,可以想象如果width为1的情况,那么应该可以更好的理解。

          然后我们来看下实验结果,这三版boxfilter在输入是2000x2000的情况下,在不同半径下的运行耗时,测试手机是华为荣耀4C(CHM-TL00),每个函数运行10次取平均为其耗时:

          可以看到第二版优化的耗时在不同半径下的表现都很稳定,基本不受影响。然后接下来的优化思路就是在确定了C++ 的代码之后可以采用arm Neon Intrinsics来加速了,就是利用向量计算指令同时处理多个数据,把独立的运算同时做,比写汇编要容易。

4.Boxfilter优化第二版 Neon Intrinsics

          上面的代码是截取列方向中间计算部分来展示如何使用arm Neon Intrinsics函数,完整代码可以看https://github.com/Ldpe2G/ArmNeonOptimization/blob/master/boxFilter/src/boxFilter.cpp#L143而行方向是没办法并行的,因为相邻元素有依赖,而列方向则可以,所以在列方向上做改写。以上代码其实挺好理解的,vld1q_f32指令就是加载4个浮点数,然后vaddq_f32,为把两个float32x4_t向量相加,相当于同时计算了4个输出,然后再把结果用vst1q_f32存回去对应的地址,然后所有参与运算的地址都是每次加4,具体可以参考官网文档

          然后来看下这版优化的耗时如何:

          可以看到耗时又少了一点,但是收益已经不大了。然后还想尝试进一步优化把Intrinsics部分改写成内联汇编试试。

4.Boxfilter优化第二版 Neon Assembly

完整版代码:https://github.com/Ldpe2G/ArmNeonOptimization/blob/master/boxFilter/src/boxFilter.cpp#L331

        这里我只对列计算中间部分做了改写,neon汇编下面的"cc","memory"之后跟的寄存器,是为了告诉编译器(主要是q开头的,q和d是一样的,q表示128位向量寄存器(16个),d表示64位(32个),q0  =(d0 + d1)),这些寄存器会在汇编内被用到,然后编译器在进入这段代码之前,要缓存这些寄存器的内容,然后在离开这段汇编之后恢复原来的值。一定要记得写上用了哪些向量寄存器。

        简单解释一下,指令的意思,"vld1.s32 {d0-d1}, [%0]!  \n",相当等于从tmpAddPtr这个地址连续读取4个浮点数到{d0-d1}也就是q0寄存器,浮点数每个32位,乘以四就是128位。最后的感叹号表示,这个指令完成之后tmpAddPtr地址加4的意思,没有就是不变。"vadd.f32 q4, q0, q2      \n" 就是把 q0和q2相加的结果放到q4,"vsub.f32 q3, q4, q1      \n" 就是把q4减去q1的结果放到q3,和上面的intrinsics指令对应,然后vst1.s32就是把寄存器的内容存到tmpOutPtr和tmpColSumPtr地址指向的内存。最后的subs指令和bne相当于for循环的功能,最后对nn减一然后bne判断是否为0, 不为0则继续循环跳到开头0标记出继续执行。

          汇编指令其实和intrinsics函数有对应的具体可参考官方文档:

https://static.docs.arm.com/ddi0406/c/DDI0406C_C_arm_architecture_reference_manual.pdf

         然后我们来看下耗时:

        什么鬼,竟然还慢了,一定是我使用的方式不对。去查了下资料,看到这篇博客里面提到,指令vld和vst都是需要消耗两个时钟周期,其他指令基本都是一个时钟周期,但是却不意味着一个时钟周期之后能立刻得到结果。那么看下来 vsub.f32 指令依赖 vadd.f32 的结果,所以白白浪费了不少时钟周期。而且现代的处理器支持双发射流水线,也就意味着CPU可以同时拾取两条数据无关指令,那么能否利用这点来更进一步加速呢。

5.Boxfilter优化第二版 Neon Assembly 第二版

完整版代码:https://github.com/Ldpe2G/ArmNeonOptimization/blob/master/boxFilter/src/boxFilter.cpp#L527

          可以看到这里的改进思路就是,把两条 vadd.f32 指令放一起,然后跟两条vsub.f32,然后把加载 vsub.f32 要用到部分数据指令 vld1.s32 放到两个 vadd.f32之间,同时 vld1.s32 指令之前加上 pld 指令,这个指令为什么能加速我问了下做移动端优化的同事,pld把数据从内存加载到cache然后下一条指令把数据从 cache加载到寄存器,如果不用pld,数据若不在cache中,那么就是需要直接从内存加载到寄存器,这里会比前者慢很多。

          然后我们来看下最终版的耗时:

        看表格最终版的耗时比起最原始的实现至少可以加速6~7倍,肯定是还有更好的优化方式,比如如果能对输入做量化把float类型数据转成8bit整型,那么就可以在单位时间处理更多数据,当然量化到8bit上计算溢出的风险也会增大许多。

          有时候炼丹炼久了,学习下优化也挺好玩的,感觉可以很好的锻炼下思维和代码能力,现在深度学习在移动端应用越来越广泛,训出来的模型如果部署到移动端之后运行的效率很低那么也是白费功夫。所以感觉对移动端优化有一定的了解对于如何设计对移动端更友好的模型还是有帮助的。

相关资料:

[1] ARM NEON 优化

[2] INTRODUCTION TO ARM ASSEMBLY BASICS

[3] 解析opencv中Box Filter的实现并提出进一步加速的方案(源码共享)。

[4] http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489c/CIHCCEBB.html


本文首发于:https://zhuanlan.zhihu.com/p/64522357

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容