面向处理器结构的C程序优化

jdygrdzh 2014-06-20 12:51:48
把常见的几种方法写下来,免得以后忘了。
面向处理器结构的优化可以从以下几个方向入手:缓存命中,指令预测,数据预取,数据对齐,内存拷贝优化,ddr访问延迟,指令优化,硬件内存管理优化。

缓存未命中,这个实际上是处理器的最大性能瓶颈之一。只要你从内存中取得数据不是用一次就丢,那么他就制约着你的性能。在powerpc上,访问一级缓存是3个时钟周期,二级是12个,3级30个,访问内存100个以上。所以如果预先取到缓存,和缓存完全未命中相比,能快30多倍。我们可以算一下,100条存取指令,100%命中和95%命中,前者300周期,后者95*3+5*100=785周期,差了1.6倍。这个结果的前提是powerpc上每个核心只有1个存取单元,哪怕多发射也无法流水到1个周期,所以最小执行时间是3个周期。当然,如果未命中的存取指令分布的好(预取和流水的妙用),开头5个未命中,后来全命中,那可能就是100+12+95*3=397,也有30%差别的。另外提一句,如果所有数据只用一次,那么瓶颈就变成了访存带宽,有点类似于显卡。所以显卡不强调缓存大小。当然他也有寄存器组,效果类似,只不过没那么大。
那么怎么提高缓存命中率?指令预测和数据预取就可以。

指令预测很常见,处理器预测将要执行的一个分支,把后续指令取出来先执行。等真正确定判断条件的时候,如果预测对了,提交结果,如果不对,丢掉预先执行的结果,重新抓取指令。此时,结果正确性不会有问题,但是会损失一点时间。
指令预测有一个方法叫做btb (branch target buffer),意思是,对于跳转指令,把它最近几次的跳转结果记录下来,作为下一次预测的依据,从而提高预测准确率。举个例子,for循环1000次,从第二次开始,每次都预取前一次的跳转地址,那么准确率接近99.9%。这是好的情况。不好的情况,在for循环里面,有个if(a[i])。这个a[i]是个0,1,0,1序列,这样每次if的预测都会错误,预取效率就很低了。改进方法,把if拆开成两个,一个专门判断奇数次a[i],一个判断偶数次,整体循环次数减少一半,每次循环的判断增加一倍,这样每次都是正确的。如果序列的数字预先不可知,只是知道0多或者1多,那么可以用c语言里面的LIKELY/UNLIKELY修饰判断条件,也能提高准确率。需要注意的是,btb表项是会用完的,也就是说,如果程序太久没有走到上次的记录点,那么记录就会被清掉,下次再跑到这就得重新记录了。

数据预取,这个和指令预测类似,也是处理器把可能会用到的数据先拿到缓存,用的之后就不必去读内存了。分为软件预取和硬件预取两种,硬件的是处理器自己有个算法去预测抓哪里的数据,比如在访问一组相同数据结构的相同元素,处理器会认为下次取的数据地址是当前地址自动加上一固定个偏移。当然,具体算法不会这么简单。软件预取就是用编译器的prefetch宏修饰某个将要用到的变量,如果处理器支持软件预取指令,编译器会把他翻译成相应指令,手工去内存抓某个程序员认为快要用到的数据。有人会问,为什么不等下抓呢?假设抓了之后,在我真正用到数据前,我有100条非内存访问指令,那么等我真正用到时候数据就在缓存了,省了很多时间。
需要注意的是,如果不是计算密集型的代码,不会跑了100个周期再有下一条存取指令。访存密集型程序可能10条指令就来一次。如果全都未命中,那么这个预取效果就会打不少折扣。并且,同时不宜预取过多数据,因为取进来的是一个缓存行,32,64或者128字节,如果取得过多,会把本来有用的局部数据替换出去。按照经验同时一般不要超过4条预取。此外,预取指令本身也至少占3个周期,过多的话,会增加每次循环执行时间。要知道有时候1%的时间都是要省的。

在访问指令或者数据的时候,有一个非常重要的地方,就是对齐。一般的四字节对齐我们都知道,在32位机器上很有用。还有一个重要的是缓存行对齐,一般是在做内存拷贝,DMA或者数据结构赋值的时候用到。我们知道处理器在读取数据结构时,是一行一起读的,比如32字节。那么,如果你的数据结构能够调整为缓存行对齐,那么就可以用最少的次数读取。在dma的时候一般都以缓存行为单位。如果不对齐,那么就会多出一些开销,甚至出错。还有,在SoC系统上,对有些设备模块进行DMA时,如果不是缓存行对齐,那么可能每32字节都会被拆成2段分别做DMA,这个效率就要差了1倍了。
还有一种需要对齐情况是数据结构赋值。假设有个32字节的数据结构,里面全是4字节元素。正常初始化清零需要32/4=8次赋值。在powerpc上,有一种指令,可以直接把缓存行置全0或1。这样时间就变成1/8了。更重要的是,写缓存实际上是需要先从内存读取数据到缓存,然后再写入。这就是说写的未命中和读未命中需要一样的时间。而用了这个指令,可以不用去读,直接写入全0/1。这在逻辑上是没问题的,因为你本来就知道要写入的数据(全0/1),不需要去读。以后如果这行被替换出去,那么就写回到内存,完全不需要读。当然,这个指令的限制也很大,就是必须全缓存行替换,没法单个字节修改。这其实就是我要说的优化后的memset函数。如果你调整下你的大数据结构,把同一时期需要清掉的元素都放一起,效率会高很多。同理,在memcpy函数里面,由于存在读取源地址和写入目的地址,按上文所述,可能需要两个读取未命中操作。现在我们可以先写入一个缓存行(没有读未命中),然后再读源,再写入,就变成了1个读取操作。写回数据那是处理器以后自己去做的事情,我们不用管。这个是不是很神奇?
标准的libc库里面的内存操作函数都可以用类似方法优化,而不仅仅是四字节对齐优化。不过需要注意的是,如果给出的地址不是缓存行对齐的,那么开头和结尾的数据需要额外处理,不然整个行被替换了了,会影响到别的数据。此外,可以把预取也结合起来,把要用的头尾东西先拿出来,再作一堆判断逻辑,这样又可以提高效率。不过如果先处理尾巴,那么当内存重叠时,会发生源地址内容被改写,也需要注意。


未完待续
...全文
232 10 打赏 收藏 转发到动态 举报
写回复
用AI写文章
10 条回复
切换为时间正序
请发表友善的回复…
发表回复
深夜航船 2014-06-24
  • 打赏
  • 举报
回复
Oprofile已经不行了,用起来太麻烦,推荐perf
jdygrdzh 2014-06-24
  • 打赏
  • 举报
回复
引用 8 楼 chenyoufu123 的回复:
Oprofile已经不行了,用起来太麻烦,推荐perf
多谢,我去了解下
jdygrdzh 2014-06-24
  • 打赏
  • 举报
回复
引用 8 楼 chenyoufu123 的回复:
Oprofile已经不行了,用起来太麻烦,推荐perf
多谢,我去了解下
jdygrdzh 2014-06-22
  • 打赏
  • 举报
回复
写完了,欢迎去我空间看看。
jdygrdzh 2014-06-21
  • 打赏
  • 举报
回复
每个现代处理器都有硬件内存管理单元,说穿了就两个作用,提供虚地址到时地址映射和实地址到外围模块的映射。不用管它每个字段的定义有多么复杂,只要关心给出的虚地址最终变成什么实地址就行。在此我想说,powerpc的内存管理模块设计的真的是很简洁明了,相比之下x86的实在是太罗嗦了,那么多模式需要兼容。当然那也是没办法,通讯领域的处理器就不需要太多兼容性。通常我们能用到的内存管理优化是定义一个大的硬件页表,把所有需要频繁使用的地址都包含进去,这样就不会有页缺失,省了页缺失异常调用和查页表的时间。在特定场合可以提高不少效率。 这里描述下最慢的内存访问:L1/2/3缓存未命中->硬件页表未命中->缺页异常代码不在缓存->读取代码->软件页表不在缓存->读取软件页表->最终读取。同时,如果每一步里面访问的数据是多核一致的,每次前端总线还要花十几个周期通知每个核的缓存,看看是不是有脏数据。这样一圈下来,几千个时钟周期是需要的。如果频繁出现最慢的内存访问,前面的优化是非常有用的,省了几十倍的时间。具体的映射方法需要看处理器手册,就不多说了。 指令优化,这个就多了,每个处理器都有一大堆。常见的有单指令多数据流,特定的运算指令化,分支指令间化,等等,需要看每家处理器的手册,很详细。我这有个数据,快速傅立叶变化,在powerpc上如果使用软浮点,性能是1,那么用了自带的矢量运算协处理器(运算能力不强,是浮点器件的低成本替换模块)后,gcc自动编译,性能提高5倍。然后再手工写汇编优化函数库,大量使用矢量指令,又提高了14倍。70倍的提升足以显示纯指令优化的重要性。 GCC的优化等级有三四个,一般使用O2是一个较好的平衡。O3的话可能会打乱程序原有的顺序,调试的时候很麻烦。可以看下GCC的帮助,里面会对每一项优化作出解释,这里就不多说了。编译的时候,可以都试试看,可能会有百分之几的差别。 最后是性能描述工具。Linux下,用的最多的应该是KProfile/OProfile。它的原理是在固定时间打个点,看下程序跑到哪了,足够长时间后告诉你统计结果。由此可以知道程序里那些函数是热点,占用了多少比例的执行时间,还能知道具体代码的IPC是多少。IPC的意思是每周期多少条指令。在双发射的powerpc上,理论上最多是2,实际上整体能达到1.1就很好了。太低的话需要找具体原因。而这点,靠Profile就不行了,它没法精确统计缓存命中,指令周期数,分支预测命中率等等,并且精度不高,有时会产生误导。这时候就需要使用处理器自带的性能统计寄存器了。处理器手册会详细描述用法。有了这些数据,再不断改进,比较结果,最终达到想要的效果。 很重要的一点,我们不能依靠工具来作为唯一的判别手段。很多时候,需要在更高一个或者几个层次上优化。举个例子,辛辛苦苦优化某个算法,使得处理器的到最大利用,提高了20%性能,结果发现算法本身复杂度太高了,改进下算法,可能是几倍的提升。还有,在优化之前,自己首先要对数据流要有清楚的认识,然后再用工具来印证这个认识。就像设计前端数字模块,首先要在心里有大致模型,再去用描述语言实现,而不是写完代码综合下看看结果。
赵4老师 2014-06-20
  • 打赏
  • 举报
回复
为什么不用CUDA呢? 参考国产银河计算机。
starytx 2014-06-20
  • 打赏
  • 举报
回复
老赵又掐秒表了
赵4老师 2014-06-20
  • 打赏
  • 举报
回复
无profiler不要谈效率!!尤其在这个云计算、虚拟机、模拟器、CUDA、多核 、多级cache、指令流水线、多种存储介质、……满天飞的时代! 使用电脑计时有时误差会很大,因为待测程序段的运行会影响电脑时钟。 将待测程序段循环足够多次,手动掐秒表计时可能更准确。
yangyunzhao 2014-06-20
  • 打赏
  • 举报
回复
膜拜一下!!
jdygrdzh 2014-06-20
  • 打赏
  • 举报
回复
再说说ddr访问优化。通常软件工程师认为内存是一个所有地址访问时间相等的设备,是这样的么?不是。我们买内存的时候,是不是有3个性能参数,比如10-10-10?这个表示访问一个地址所需要的三个操作时间,命令,数据还有预充电。前两个好理解,第三个的意思是,我这个页或者单元下一次访问不用了,必须关闭,保持电容电压,否则再次使用这页数据就丢失了。根据这个原理,ddr地址有三个部分组成,列,行,页。如果连续的访问都是在同行同列同页,每一个只需要10单位时间;同行不同列同页,30单位;不同行同列同页,20单位。所以我们得到什么结论?相邻数据结构要放在一个页,并且绝对避免出现同行不同列同页。这个怎么算?每个处理器都有手册,去查查物理内存地址到内存管脚的映射,推导一下就行。此外,ddr还有突发模式,ddr3为例,64位带宽的话,可以一个命令跟着8次读,可以一下填满一行64字节的缓存行。而极端情况(同页访问)平均字节访问时间只有10/64,跟最差情况,30/64字节差了3倍。是不是豁然开朗了?你这么去分析程序,小伙伴们会惊呆的。当然,内存里面的技巧还很多,比如故意哈希化地址来防止最差情况访问,两个内存控制器同时开工,并且地址交织来形成流水访问,等等,都是优化的方法。不过通常我们跑的程序由于调度程序的存在,地址比较随机不需要这么优化,优化有时候反而有负面效应。 未完待续
《LabVIEW高级编程与虚拟仪器工程应用(修订版)》适用有一定LabVIEW编程基础的测控工程技术人员,帮助其搭建高级技术框架,积累开发经验;同时也可作为本科生毕业设计、研究生完成课题和工程技术人员开发测控项目的参考用书。 目录 第1篇LabVIEW高级编程技术 第1章测控项目管理 1.1测控项目的生命周期 1.2系统定义 1.2.1 问题定义 1.2.2可行性研究 1.2.3 需求分析 1.2.4软件原型 1.2.5 文档管理 1.3总体设计 1.3.1硬件结构设计 1.3.2软件结构设计 1.3.3 总体设计说明书 1.4详细设计 1.5程序编码 1.5.1编程风格 1.5.2说明信息 1.5.3 vl的保存 1.5.4手册编写 1.6系统测试 1.6.1硬件测试 1.6.2软件测试 1.6.3验收测试 1.6.4测试报告 1.7 系统维护 1.8 项目浏览器 1.8.1 项目浏览器的用途 1.8.2 项目库 1.8.3项目依赖关系 1.8.4程序生成规范 第2章应用程序控制与内部数据传递 2.1 VI Server技术简介 2.2动态加载VI 2.2.1监测内存中所有的VI 2.2.2 动态加载VI的程序 2.3动态控制VI运行 2.3.1 动态刷新被控VI前面板控件值 2.3.2选择性打开VI前面板 2.3.3子面板设计 2 4动态控制VI属性和前面板对象属性 2.4.1 动态控制VI属性 2.4.2动态控制前面板对象属性 2.5动态注册事件 2.5.1 动态注册用户接口事件 2.5.2处理用户事件 2.6运行菜单控制 2.6.1运行菜单的设置 2.6.2用程序代码进行运行菜单设置 2.6.3在程序中响应菜单选项 2 7通知器和队列 2.7.1 通知器 2.7.2 队列 2 R共享变量 2.8.1共享变量的创建 2.8.2单进程共享变量 2.8.3 网络发布共享变量 第3章程序设计模式与程序性能 3.1 程序的设计模式 3.1.1标准状态机 3.1.2主/从设计模式 3.1.3 生产者/消费者设计模式 3.1.4队列消息处理器 3.1.5其他设计模式 3.2程序调试技巧 3.3多线程程序 3.3.1基本定义 3.3.2 多线程应用程序的优势 3.3.3 LabVIEW实现多线程的方法 3.4程序性能优化 3.4.1程序运行速度 3.4.2 内存使用 3.5程序性能分析 第4章软件接口与外部数据通信 4.1 ActiveX技术应用 4.1.1 ActiveX技术简介 4.1.2使用ActiveX控件 4.1.3使用ActiveX自动化 4.1.4 LabVIEW作为ActiveX服务器 4.2.NET技术应用 4.2.1.NET技术简介 4.2.2 .NET技术应用 4.3动态数据交换 4.3.1 LabVIEW的DDE功能 4.3.2向Excel文件写数据 4.3.3 由Excel文件读数据 4.4 C代码调用 4.5库函数调用 4.6执行操作系统命令 4.7计算机网络基础知识 4.7.1 计算机网络的功能与发展 4.7.2计算机网络的结构 4.7.3 计算机网络模型 4.7.4计算机网络协议 4.8 TCP 应用 4.8.1发送数据编程 4.8.2接收数据编程 4.8.3程序的远程动态控制 4.9 UDP应用 4.10 串口通信 第5章数据存储与调用 5.1数据存储的时机 5.2打印报表 5.3数据库连接 5.3.1 LabVIEW与数据库的连接 5.3.2 LabVIEW对Access数据库的操作 5.3.3 LabVIEW对SQL Server数据库的操作 5.4 LabVIEW与实时数据库连接 第6章面向对象编程 6.1面向对象编程的概念 6.2 LabVIEW中面向对象编程的方法 6 2.1 LabVIEW类 6.2.2类的方法 6.2.3继承 6.3两种编程方法的比较 6.3.1测试目的 6.3.2面向过程的方法 6.3.3 面向对象的方法 6.3.4 两种方法的比较 第7章传统DAQ的模拟信号采集 7.1 硬件配置与测试 7.1.1传统DAQ安装 7.1.2传统DAQ设备配置与测试 7.2模拟输入 7.2.1传统DAQ的数据采集通道 7.2.2数据采集Vl 7.2.3传统DAQ模拟输入常用的基本术语 7.2.4测量直流电压信号 7.2.5 波形采集 7.2.6频率测量 7.3模拟输出 7.3.1输出直流信号 7.3.2输出波形信号 7.3.3 模拟输入/输出控制回路 7.4信号调理 7.4.1信号调理设备配置 7.4.2应变测量 7.4.3温度测量 第8章传统DAQ的数字信号与计数器输入/输出 8.1 数字信号输入/输出 8.1.1数字信号通道设置 8.1.2立即方式数字输入/输出 8.1.3握手方式数字输入/输出 8.2计数器输入/输出 8.2.1数据采集卡的计数器芯片 8.2.2 用计数器输出脉冲信号 8.2.3测量脉冲宽度 8.2.4测量TTL信号频率和周期 8.2.5事件计数与计时 第2篇虚拟仪器工程案例篇 第3篇C语言代码转换和LabVIEW2010新特性篇 LabVIEW常用中英文词汇对照表 参考文献

69,371

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧