国家集训队2009论文集论程序底层优化的一些.pdf
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_1.gif)
![资源得分’ title=](/images/score_05.gif)
《国家集训队2009论文集论程序底层优化的一些.pdf》由会员分享,可在线阅读,更多相关《国家集训队2009论文集论程序底层优化的一些.pdf(37页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、2009 年全国信息学奥林匹克冬令营论文 成都七中 骆可强 第 1 页 论程序底层优化的一些方法与技巧 成都七中 骆可强 摘要:本文以优化程序运行的时间效率为目地,从编译器、汇编代码、CPU 特性等较为底层的概念着眼,对程序优化进行了全方位的探讨,总结了在优化中实用的思想、原则、方法和技巧,并对它们在竞赛中的应用价值做出了一些尝试。关键字:优化 CPU 汇编语言 编译器 目录 序言 第页 引例 第页 CPU 指令的运行效率 第页 数值运算的优化 第页 除法 第页 乘法 第页 高精度运算 第页 CPU 优化特性 第页 高速缓存 第页 分支预测 第页 乱序执行 第页 位运算技巧 第页 高维数组使用
2、的注意事项 第页 应用举例 第页 总结 第页 参考文献 第页 特别感谢 第页 序言 信息学奥林匹克竞赛(Olympiad in Informatics)是研究怎样编写计算机程序来解决特定问题的竞赛。考察的关键点,在于怎样利用有限的系统资源(CPU 时间片与系统内存)来求解规模庞大的数学模型。在“正确”这一前提下,“效率”自然是考虑问题的第一要素。效率,分为时间效率与空间效率,如何对时间效率进行优化是本文将要研究的主题。算法算法是决定时间效率的关键是决定时间效率的关键 优化程序的时间效率,简单地讲,就是用尽一切手段,在保证正确的前提下让程序的运行时间更短。那么,有些什么手段呢?最重要的自然是:使
3、用尽可能高效的算法。算法(Algorithm),是一系列解决问题的机械步骤,它采用明确定义的语义,描述了求解特定数学模型的一般方法。算法的好坏,直接决定了程序的运行效率。采用低效的算法或高效的算法,其差别就好像选择走路或是坐飞机,完全不在一个数量级。时间复杂度时间复杂度的概念的概念及其局限性及其局限性 2009 年全国信息学奥林匹克冬令营论文 成都七中 骆可强 第 2 页 为了衡量一个算法时间效率上的优劣,计算机科学中引入了时间复杂度的概念。回忆我们习惯使用的大 O 表示法,我们说一个算法运行时间的界是 O(f(n),所表示的意义是,假设这个算法的实际运行时间关于输入规模 n 的函数是 T(n
4、),那么存在正常数 n0、c,使得对于 n n0,有 0T(n)cf(n)。有了这样一个上界,我们就能知道 T(n)的增长速度,从而能够大致判断对于给定的 n,我们的算法能不能在一个合理的时间内出解。然而另一方面不可否认的是,这样一个工具是极其粗略的。我们注意到刚才的定义式中存在一个常数 c,它在渐进意义上是无关紧要的,但回到现实世界中,它却不可忽视。同样是 O(n)阶的算法,有些可以快到只消耗 2n 个 CPU 时钟周期,而另一些甚至需要 1000n 个时钟周期还要多。就好像同样是乘坐飞机,也有快慢的极大差别。使用相同时间复杂度的算法,因为这个常数 c 的不同,实际运行所需要的时间,也可能有
5、天壤之别。算法并不是时间效率的全部算法并不是时间效率的全部 那么,这个常数受哪些因素的影响呢?无疑,它同样受制于算法:不同的算法,可能有着相同的复杂度,但是实际效果截然不同。相同的算法,可能有着不同的实现方式,一些逻辑上的简化也能大大降低运行所需花费的时间。那么,算法就是程序运行效率的全部了么?答案是否定的,有些东西是隐藏在逻辑层面之下的,它们同样显著地影响着程序的运行效率,而我们却很难看到。举例来说,你能想象当我们在 C 语言中书写 a/=7 这一语句时,实际上处理器并没有做缓慢的除法,而使用了乘法和位移取而代之么?因为这样,我喜欢把大 O 定义式中的常数 c 一分为二的来看待:cc1c2,
6、c1 代表逻辑层面(算法)的消耗,而 c2 表示每一句程序语句在底层运行的消耗,那么程序的实际运行时间,约为 c2c1f(n),方括号中的部分 c1f(n)就完全由算法来决定,而 c2 则取决于程序的底层实现。前者固然重要,后者也同样不可忽视。本文将要研究的,就是怎样在程序运行的底层对细节做出优化,以提升程序的运行效率。为什么要学习底为什么要学习底层优化层优化 在 OI 竞赛中,算法是考察的重点。底层实现看起来并不重要,确实提升空间也相对较小,但当我们设计的算法有一些先天缺陷时,或许对底层做细致的优化能对我们有很大的帮助,在后文中会展示一些实际的例子。在竞赛之外,学习底层的东西,能让我们更深入
7、地认识眼前的机器,即使在使用高级语言书写程序时,脑海中也会自然投射出底层发生的事情,从而能够写出质量更高的代码。在这篇论文前期准备、实验研究、总结规律到最终成文的过程中,我学到了太多的东西,这些东西在我们平时为 OI 竞赛编程的过程中是很难看到的,而我也相信,这些东西在为大家所广泛认识之后,同样能够服务于竞赛,实际地提升成绩。说了这么多,或许是该展现一个实例的时候了。下面我们从一个极其简化的编程任务入手,来看看什么是底层优化,有些什么样的工具,能做到什么程度。2009 年全国信息学奥林匹克冬令营论文 成都七中 骆可强 第 3 页 引例 让我们先来看一个非常简单的例子:假设我们被要求编写这样一个
8、函数,接受一个指向 32 位正整数数组头部的指针,以及数组的长度,要求函数返回这些整数中的最大值。在 C 语言中,函数头应该是这样:朴素朴素的实现的实现 若要在算法层面研究这个问题几乎没有任何价值,进行一遍线性的扫描即可得到 O(n)级别的算法,另外由于数组中的每一个元素必须被访问到才能保证出解正确,所以算法的复杂度下界也是 O(n)。我们可以立即写出下面这个 C 语言程序:int get_max(int*a,int l);平台简介平台简介 条件所限,本文中的所有研究、编码、测量工作均在一台 Thinkpad X61 笔记本上进行,下面简要给出工作平台上硬件和软件与优化工作相关的规格参数:CP
9、U:Intel Core 2 Duo CPU T7100 1.80GHz Cache:L1-64KB L2-2MB 主存:SODIMM Synchronous 667 MHz 1GB 2 操作系统:Ubuntu 8.10-Intrepid Ibex 内核版本:Linux 2.6.27-8-generic 编译器版本:gcc 4.3.2 汇编器版本:GNU assembler 2.18.93.20081009 连接器版本:GNU ld 2.18.93.20081009 当然,一种优化方法的实际效果,和平台规格密切相关。如果只在一种平台(主要指 CPU)进行测试就妄下结论,是不负责任的。为此,我也
10、将文中所涉及的程序在不同的平台下进行了广泛的测试,跨越了不同的操作系统(windows 与 linux)和不同厂家生产的 CPU(intel 与 AMD)。测量结果的绝对值自然千差万别,但采取不同优化方式所取得的效果基本上是一致的,这得益于现代 CPU 都采用了一套相似的内部架构与优化引擎,为开发人员提供了方便。不过另一方面,也要求我们在学习 CPU 开发时应以把握大原则为重点,而不要钻牛角尖。如果沉溺于为某种特定的 CPU 内部特性做古怪的优化,会导致在不同的 CPU 上运行效果天差地别,这样的优化是没有价值的。遗憾的是,后文将提及的 SIMD 系列优化方法,在 AMD 生产的芯片上,虽然可
11、以兼容,但是优化效果并不突出,值得大家留意。/最初版程序 int get_max(int*a,int l)int mx=0,i;for(i=0;imx)mx=ai;return mx;2009 年全国信息学奥林匹克冬令营论文 成都七中 骆可强 第 4 页 这个程序显然能够正确地完成所要求的任务,而且,算法的复杂度也达到了理论下界。效率怎么样呢?效率怎么样呢?让我们来检验一下程序运行的速度。我在测试平台将此函数应用于一个存有 10000000个整数的数组,连续运行 100 次并求平均占用的时钟周期数。测量值为 75305510 个周期。处理一个数平均就占用了 78 个时钟周期,结果不令人满意。第
12、一次优化第一次优化 开始着手寻找程序可优化之处,首先发现ai被提及了两次,重复计算地址在这个小循环中的开销也是不可忽略的,那么第一版的优化我们尝试来进行普通的循环和寻址的优化:关于文中程序所使用的计时方法关于文中程序所使用的计时方法 为了测试程序的运行效率,需要一种测量时间的工具,有许多不同的库函数,操作系统 api,命令行工具可以进行时间测量,本文主要使用 IA32 处理器提供的 rdtsc 指令来获取程序运行消耗的时钟周期,此方法精确度较高,缺点在于无法排除掉程序运行过程中操作系统和后台运行程序所占用的时钟周期,结合 linux 系统命令 time 一起使用可以弥补这一缺憾.下面是本文所有
13、程序都会用到的计时函数的 C 代码:#define ull unsigned long long ull get_clock()ull ret;_asm_ _volatile_(rdtscnt:=A(ret):);return ret;编译说明编译说明 众所周知,现代编译器已经不再局限于简单地把一句句高级语言翻译为对应的汇编语言,而能够智能地完成许多以前只有人手工才能完成的优化,特别是各个编译器提供的高级优化选项,更是常常能够提供接近人类手工优化的效率。但是,在我们 OI 竞赛的评测中,这些优化选项是不会被开启的。所以,本文中所有程序的编译,都没有打开优化选项。诚然,打开这些优化选项,本文中介
14、绍的各种人工优化的效果可能会打些折扣,但我仍然觉得这样做是合理的,一来更贴近比赛中使用的评测环境,二来既然是在研究优化,我们自然应该亲自去探究其中的方法和原理,而不应该让别人的程序来代劳,否则可能永远也不会清楚其中的奥妙。为此,本文中所有的程序均使用竞赛时的编译选项进行编译并测量效果,对于 c语言程序,编译命令为:gcc a.c-o a 2009 年全国信息学奥林匹克冬令营论文 成都七中 骆可强 第 5 页 测试一下运行时间,平均占用时钟周期测量值为:66047005,处理一个数平均下降了一个时钟周期,比第一版程序在时间效率上优化了 12%。来回顾一下这次优化,他解决了在初版程序代码中出现的重
15、复寻址的问题,而获得了期望中效率的提升,从本质上说仍然属于逻辑层面的优化。这次优化是在 C 语言层面编写,也能够在 C 语言层面进行解释的。第二次优化第二次优化 继续前面的思路,很难再想出什么有效的优化了,这就是我们的终点吗?其实游戏才刚刚开始,下面给出第二个优化:这个程序使用了许多宏定义,可读性较差。不过思路非常简单:将原来单线的求最大值进程分为八路,最后再来汇总总的最大值。要说明一点的是,正如程序第一行的 assert所显示,这个函数只能在 l 是 8 的倍数的情况下工作,要让它可以对任何 l 工作很容易,不/第二次优化 int get_max(int*a,int l)assert(l%8
16、=0);#define D(x)mx#x=0 int D(0),D(1),D(2),D(3),D(4),D(5),D(6),D(7),*ed=a+l;#define CMP(x)if(*(a+x)mx#x)mx#x=*(a+x);while(a!=ed)CMP(0);CMP(1);CMP(2);CMP(3);CMP(4);CMP(5);CMP(6);CMP(7);a+=8;#define CC(x1,x2)if(mx#x1mx#x2)mx#x2=mx#x1;CC(1,0);CC(3,2);CC(5,4);CC(7,6);CC(2,0);CC(6,4);CC(4,0);return mx0;/第
17、一次优化 int get_max(int*a,int l)int mx=0,*ed=a+l;while(a!=ed)if(*amx)mx=*a;a+;return mx;2009 年全国信息学奥林匹克冬令营论文 成都七中 骆可强 第 6 页 过那不是我们关注的重点,为了让代码简短一些,略去不表。这个程序的效率怎样呢?同样平台下的测量值为:34818706,比最初版本优化了 54%。是什么让这个程序拥有了如此大的效率提升?难道是指针 a 的递增次数只有原来 1/8导致的?从 C 语言的层面来看,只有这一个解释。但这是说不通的,因为相比循环体里面的语句,这一个递增显得无足轻重,至少不会带来这么大的
18、效率改善。而除了这里,从 C 代码上看和第二个程序又没有本质上的区别。难道计算机程序的运行是完全不可预测和理解的吗?深入深入汇编语言汇编语言 让我们来回顾一下 C 语言编写的程序是怎样运行的吧,首先编译器(compiler)将 C 代码编译为汇编语言(assembly language)代码,再经过汇编,连接等一系列步骤转化为可执行文件。这里最关键的一部,自然在编译环节,因为一旦代码编译为机器指令后,在 CPU中执行它的方式就已经确定了。编译的质量,也直接决定了程序运行的效率。如果仅仅把目光集中在 C 代码的层次,而完全不在汇编语言层面进行思考,优化的过程就像被蒙住了眼睛。既不可能真正看清程序
19、运行效率的本质,也不能进一步进行更强的优化。对第二次优化的程序进行编译,得到相应的汇编代码,经过查看,发现除了循环变量的累加之外,其他语句对应的汇编代码的面目并无甚差别,仅仅是简单的重复并列起来。但确实获得了极大的效率提升,要完整的解释这个问题,会提及处理器中的各种优化机制:乱序执行、流水线机制、指令预取、分支预测、寄存器重命名、高速缓存,这些主题都将在后文中作研究。简单的讲,在最初两个程序中,每次计算新的 mx 都会依赖于上一步的计算结果,相关的计算指令也必须依次运行,而将求值过程分为多路处理,mx0,mx1 等变量的相关指令之间互相没有关联,让处理器有更大的机会将他们并发。过于底层的细节固
20、然不容易完全掌控,但是遵循一些基本的原则,总有机会使处理器为我们作出优化。第三次优化第三次优化 既然已经深入到了汇编语言的层次,那不如直接用汇编语言来编写这个函数。汇编语言的利与弊汇编语言的利与弊 在 CPU 中重要的概念如寄存器(register)、状态标志(status flag)、指令(instruction)等,在高级语言中全部被隐藏,取而代之的是,高级语言过于依赖内存变量这一概念,而读写内存,是处理器最低效的操作之一。而且高级语言过于丰富的语义也造成了翻译过程自然出现极大浪费。这一切,让我们在 C 语言层面作优化,就像带着脚镣跳舞,一旦使用起汇编语言,这一切就豁然开朗了,我们不再被内
21、存变量与高级语句所束缚,可以直接操纵最底层的部件,更有额外的丰富指令可供使用。可以说,使用汇编语言是获得极致效率的基础。汇编语言既然如此之好,那我们为什么不用汇编语言来编写所有的程序呢。这主要是因为汇编语言编码成本太高,要用来编写大型程序虽并非完全不可能,也是极其困难的,实际的程序中并非所有地方都需要如此极致的效率,而真正需要的是正确地设计代码架构并编写程序逻辑,要用汇编语言来做这些,就好像用小颗粒的颜料逐点绘制一副油画,是很难把握成品的全貌的。另一方面,汇编语言编写的程序会有较强的平台依赖性,可移植性很差,也使它被拒于应用程序开发的门外,而高级语言(如 C 语言)被发明出来较好地解决了这些缺
22、陷。那么汇编语言又一钱不值了吗?当然也不是,就算在现今的实际程序开发过程中,程序员们仍然使用汇编语言来编写程序中的效率瓶颈部分,而程序的主要部分使用高级语言来2009 年全国信息学奥林匹克冬令营论文 成都七中 骆可强 第 7 页 编写。要实现这一点,最简便的方法是使用编译器各自提供的内嵌汇编机制。使我们可以直接在 C 代码中书写汇编代码,并且可以通过 C 编译器的编译。这样做还有一个最直接的好处:在竞赛中,我们可以直接提交这样的程序并通过编译。使用内嵌汇编进行优化使用内嵌汇编进行优化 下面正式开始转向汇编语言进行优化,本文中的程序均使用 gcc 内嵌汇编,采用 AT&T格式的汇编语法,后面不再
23、做说明。首先,我们直接朴素地将原始意图使用汇编语言实现一遍,得到如下代码:这个程序的效率如何呢?测量结果为:平均 21322853 个时钟周期。处理一个数据平均只需要 2 个时钟周期了,相比最初的程序,优化了 72%,结果十分令人满意。打量一下这个程序,核心循环中,有 5 条指令,其中甚至有两条是条件分支指令,还有两条需要访问内存,而且使用了最复杂的 sib 寻址方式。感觉起来,平均 2 个时钟周期,是没有道理的,其实这主要得益于现代 CPU 各种强大的优化机制:高速数据 cache 使两次访问同一内存如同访问寄存器一般迅速,第一个条件跳转大部分时间不会成立,而相反第二个跳转总会成立,这让 C
24、PU 的分支预测发挥到极致。而强大的乱序执行引擎使得循环中的这些小指令得以以接近双倍的时间运行(以上提到的这些名词在后文都会有详细的介绍)。现代CPU 的优化如此强大,是否我们可以胡乱书写汇编代码了呢?绝对不是。优化中的一些小插曲优化中的一些小插曲 声明声明 本文并非以讲授汇编语言与计算机体系结构为目的,所以在这里假设读者对这两者有最基本的了解,否则阅读会有较大障碍。/第三次优化 int get_max(int*a,int l)int ret;_asm_ _volatile_(movl$0,%eaxnt .p2align 4,15n LP1:nt cmpl-4(%1,%2,4),%eaxnt
25、jge EDnt movl-4(%1,%2,4),%eaxn ED:nt /loop LP1nt decl%2nt jnz LP1nt movl%eax,%0nt :=m(ret):r(a),c(l):%eax);return ret;2009 年全国信息学奥林匹克冬令营论文 成都七中 骆可强 第 8 页 注意到在上面的程序中,有一行注释掉的 loop 指令,如果没有经验,或许会想,使用 loop 指令,指令码体积更小,而且使用复杂指令,一次性指示 CPU 完成更复杂的工作,应该具有更高的效率。那么我们使用 loop 指令取代递减与跳转指令试一试,结果令人大跌眼镜:平均耗费 56457348
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 国家 集训队 2009 论文集 程序 底层 优化 一些
![提示](https://www.taowenge.com/images/bang_tan.gif)
限制150内