Upload
others
View
9
Download
0
Embed Size (px)
Citation preview
飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
嵌入式Linux性能优化
2飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
嵌入式Linux性能优化
• 性能评价
• 性能优化的流程
• 性能评测
• 优化的基本原则
• Shell脚本优化
• C和C++程序优化
3飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
性能评价
• 如何定义快和慢?– 快和慢与个人感官密切相关,有的人认为快,有的人认为慢,我
们需要在整个开发过程中参与的人员有一个共同的标准。
• 为每一个用户的操作,定义具体的时间值
– 一般精度在1/10秒
– 为一个设备所定义的性能指标,可能达到几十个。
2000010000关机
3000021000开机
版本2版本1下限理想性能
4飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
性能优化的流程
• 性能优化是一个不断迭代的过程,不要认为一蹴而就。
5飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
性能的评测
• 摄像头方式(测试人员使用居多)
– 优点:
• 使用起来非常简单,不需要熟悉代码。
• 更加接近用户实际的感受。
– 缺点:
• 时间的精确度受摄像头拍摄帧数的限制,一般精度不高,误差在100ms左右。
• 代码中增加log(开发人员使用居多)
– 优点:
• 测试时间的精确度将更高,能够精确到1ms左右。
• 能够灵活的定位代码中各个部分所占用的时间。
– 缺点:
• 在代码中增加时间的log,需要对代码非常的熟悉。
• 从log中得出的时间,往往因为各种意外情况的出现,往往会与用户的实际感受不一致。
6飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
性能分析
• 导致程序运行效率低下,主要有下面3种原因:
– 程序的运算量很大,导致CPU过于繁忙,CPU是
瓶颈。
– 程序需要做大量的I/O,读写文件、内存操作等等,CPU更多的是处于等待,I/O部分称为程序性
能的瓶颈。
– 程序之间相互等待,结果CPU利用率很低,但运
行速度依然很慢,事务间的共享与死锁制约了程序的性能。
7飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
性能分析
• 对于大量IO操作所引起的性能问题,我们可以考虑使用异步IO方式来提高程序性
能。
推荐一篇文章:http://www.ibm.com/developerworks/cn/linux/l-async/
• 后面所有的内容,主要针对程序量大,CPU是瓶颈的问题。
8飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
优化的原则
• 等效原则:优化前后程序实现的功能一致。
• 有效原则:优化后要比优化前运行速度快或占用存储空间小,或二者兼有。
• 经济原则:优化程序要付出较小的代价,取得较好的结果。
9飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
Shell脚本优化
• 在Linux系统中,我们会经常使用到shell脚本,我这里只介绍由busybox实现shell引擎的脚本优化。
• 在busybox中,命令分为三种类型:
– APPLET:也即我们所说的applets,它由busybox创建一个fork出一个子进程,然后调用exec执行相应的功能,待执行完毕后,返还控制给父进程。
– APPLET_NOEXEC:系统将调用fork创建子进程,然后执行busybox中对应的功能,在执行完毕后,返回控制给父进程。
– APPLET_NOFORK:它相当于builts-in,只是执行busybox的内部函数,不必创建子进程,所以其效率高。
10飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
Shell脚本优化
• 在Linux中调用fork、exec是很花时间的,所以我们应该使用APPLET_NOFORK命令,其次是APPLET_NOEXEC, 后APPLET。
• 在busybox 1.9中,属于APPLET_NOFORK的功能有:
[ basename cat dirname echo false hostid length logname mkdir pwd rmrmdir seq sleep sync touch true usleep whoami yes
• 属于APPLET_NOEXEC的功能有:
awk chgrp chmod chown cp cut dd find hexdump ln sort test xargs
例如:
printf “hello world\n”echo “hello world\n”
11飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
Shell脚本优化
• 在bash脚本中命令,还有一些特殊的约定:
– 包含在pipe中的builts-in将被创建子进程来执行。
– 包含在 ` 的命令将创建子进程来执行。
• 优化busybox bash脚本
– 去掉无用的脚本
– 尽可能使用busybox 内部实现的命令
– 尽量不要使用pipe– 减少pipe中的命令数
– 尽量不要使用`我们可以参考:
http://tree.celinuxforum.org/CelfPubWiki/OptimizeRCScripts。
12飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 查看进程启动速度
– # strace -tt ./hello23:31:02.618348 execve("./hello", ["./hello"], [/* 9 vars */]) = 023:31:02.624391 uname({sys="Linux", node="(none)", ...}) = 023:31:02.628785 brk(0) = 0x1100023:31:02.631655 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)23:31:02.634035 open("/etc/ld.so.cache", O_RDONLY) = -1 ENOENT (No such file or directory)23:31:02.636079 open("/lib/tls/v6l/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or
directory)…….……23:31:02.664339 close(3) = 023:31:02.667817 mprotect(0x412b8000, 4096, PROT_READ) = 023:31:02.670321 fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 023:31:02.672884 mmap2(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40000000
13飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 查看进程启动过程
– # LD_DEBUG=libs ./hello432: find library=libc.so.6; searching432: search cache=/etc/ld.so.cache432: search path=/lib/tls/v6l:/lib/tls:/lib/v6l:/lib:/usr/lib/tls/v6l:/usr/lib/tls:/usr/lib/v6l:/usr/lib (system
search path)…………432: trying file=/lib/libc.so.6432: calling init: /lib/libc.so.6432: initialize program: ./hello432: transferring control: ./hello– # LD_DEBUG=help ./helloValid options for the LD_DEBUG environment variable are:libs display library search pathsreloc display relocation processingfiles display progress for input file
……….– help display this help message and exit
14飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 进程启动过程
– 搜索其所依赖的动态库。
– 加载动态库
– 初始化动态库
– 初始化进程
– 将程序的控制权,移交给main函数。
15飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 减少加载动态库的数量
– 使用dlopen,将启动时不需要的动态库,延后加载
– 将一些动态库,改为静态库
• 优点:
– 减少了加载动态库的数量。
– 在与其他动态库(或进程)合并之后,动态库内部之间的函数调用不必再进行动态链接、符号查找,从而提高速度。
• 缺点:
– 该动态库如果被多个动态库或进程所依赖的话,那么该动态库将被复制多份合并到新的动态库中,导致整体的文件大小增加,占用更多的flash。
– 失去了动态库原有的代码段内存共享,因此可能会导致内存使用上的增加。
16飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 加载动态库时的搜索路径:
– HWCAP机制
– DT_NEED入口中包含的路径
– DT_RPATH入口给出的路径(存在的话)
– 环境变量LD_LIBRARY_PATH路径(setuid类的程序排除)
– LD_RUNPATH入口给出的路径(存在的话)
– 库高速缓存文件ld.so.conf中给出的路径
– /lib /usr/lib
17飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• HWCAP机制
– 在各个不同的系统种,系统的硬件功能是不一致的,有的系统支持浮点运算,有的则不支持。这样,由于系统硬件功能的不同,软件需要加载不同的共享库,glibc的HWCAP就是为了这个功能所设
计的。根据不同的硬件特性,到不同的目录去搜索动态库,来实现前面提到的需求。
• 禁止HWCAP
– # export LD_HWCAP_MASK=0x00000000
18飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 优化动态库搜索路径
– 设置LD_HWCAP_MASK,禁掉一些不用的硬
件特性。
– 将所有的动态库都放在一个目录下,并将该目录放在LD_LIBRARY_PATH的开始。
– 不能放在一个目录的,在进程中加入-rpath选项,指定搜索路径。
19飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 动态库的初始化
– 动态库的构造和析构函数机制。
– 动态库的全局变量初始化工作。
20飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 动态库的构造函数与析构函数
void __attribute__ ((constructor)) my_init(void)
void __attribute__ ((destructor)) my_fini(void);
• 对于内置类型的全局变量,存储在data段,在加载动态库时,只需mmap将其映射到内存中即
可。
• 对于非内置类型的全局变量,其存储在bss节,在使用mmap映射到内存之后,还需要运行其构造
函数将其构造出来。
21飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 动态库 liba.soclass c1{public:
c1();int c1_i1;
};c1::c1(){
printf("library 0\n");};c1 g1;• # ./hello• library 0
22飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 对于某些全局变量,其在进程启动过程中并不需要,但它仍然会在加载动态库和进程时,运行期构造函数,会导致:
– 构造函数中修改成员变量,dirty page不必要的增加。
– 运行构造函数,导致进程的启动速度变慢。
• 减少非内置类型全局变量的使用
– #nm -f sysv hello | grep bss– 在bss节,其为OBJECT,不包含函数名,对象大小
>4,基本可以认为是全局对象。
– 在bss节,其为OBJECT,不包含函数名,对象大小=4,有可能是全局对象,需要你到代码中去搜索、确认。
23飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 动态链接
– 减少导出符号的数量
gcc a1.cc –-version-script=sym.map –o test
– 减少符号的长度,加快符号匹配的速度
– 使用Prelink
24飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 使用Prelink– 配置prelink.conf,在该文件中增加所要做预先链接的进程和动态库所在的路
径。
Prelink.conf# If a directory name is prefixed with `-l ', the directory hierarchy# will be walked as long as filesystem boundaries are not crossed.# If a directory name is prefixed with `-h ', symbolic links in a# directory hierarchy are followed.-l /lib-l /usr/lib-l /usr/local/lib
– 运行prelink命令,将prelink.conf里面路径下的所有ELF文件,进行预先链接。
# prelink -afmR –c /etc/prelink.conf
25飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• Prelink都做了哪些事情:
– 分析所有的进程和动态库,为每个动态库指定一块唯一的内存地址。
• 总是一同出现的动态库,其动态库的加载地址一定不能重叠;
• 总是不同时出现的动态库,其动态库的加载地址可以重叠。
– 分析进程和动态库中,所有需要重定位的函数,全局变量等,使用loader进程符号查找,对齐地址进行解析。
– 修改进程和动态库的二进制文件。
• 使用Prelink的先决条件:
– 在编译你的动态库时,加上-fPIC选项,生成位置无关代码。
– Glibc的版本要高于2.3.1– Prelink对于dlopen打开的动态库没有效果。
26飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 如果我们做了前面所有的工作,仍然无法满足进程启动速度的要求,那么我们只能在调度上下功夫。
– 进程改为线程
– Preload进程
– 提前加载,延后退出
– 调整CPU频率
27飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 进程改为线程
– 我们可以把原来的进程分割为两个部分:
• 常驻内存部分,其为daemon进程,主要负责加载进程所需要的动态库,侦听用户信号,创建和销毁用户逻辑线程;
• 完成用户逻辑部分,由daemon部分创建线程,按用户需求完成用户逻辑。
– 这样就节省掉了加载动态库、初始动态库和全局变量部分,可以缩短进程的响应时间,来满足用户需求。
– 我们还可以再引申一下,将原来的多个daemon进程的常驻内存部分进行合并,根据用户逻辑的需求,创建不同的线程。
28飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 进程改为线程
– 好处:
• 创建线程时,不需要重新加载动态库,故缩短了进程的响应时间
• 多个业务逻辑共享动态库,避免了系统为每个业务逻辑创建动态库的数据段,从而节省了大量的内存。
– 缺点:
• 由原来的进程改为线程,工作量比较大,代码修改上存在一定的风险。
• 多个业务逻辑线程之间共享动态库,有可能会带来全局变量的冲突。
• 由于还是存在daemon进程部分,所以其堆所栈内存不会被释放,多个业务逻辑线程所存在的内存泄漏会纠缠在一起,从而是问题更加复杂。
29飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• Preload进程
– 我们在进程的main函数中,插入一行语句
pause();这样,当进程启动时,加载完动态库后,就会停在这里,不会运行用户逻辑。
– 当我们需要响应用户时,向该进程发送一个信号,这样用户就会继续前进,处理用户逻辑,这样我们就节省了进程加载动态库的过程。
这里我们,需要注册一个信号处理函数。
void sigCont(int unused){return ;
}int main(int argc, char** argv){
signal(SIGCONT,sigCont);pause();
– 当用户逻辑执行完成后,就退出进程,同时再启动该进程,这时进程会在加载完动态库后,停留在那里。
30飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• Preload进程
– 好处:
• 该方法对于目前的进程来讲,修改起来十分容易。
• 由于没有了daemon部分,堆段在进程退出后,所以进程的内存泄漏问题影响不大。
– 缺点:
• 由于在进程退出后,需要重新启动进程,会带来调度上复杂性。
• 在进程等待的时候,其加载的动态库还是要占用一部分内存。
• 由于该方法只是将进程的启动分为了两个部分,并没有在整体上缩短进程的启动时间,故对于那些需要频繁启动、退出的进程效果并不理想。
31飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 当进程启动速度无法满足时,我们往往想到了将其提前加载(在开机时启动),却没有想到其退出条件,而导致进程中又多了一个daemon进程。
• 提前加载,延后退出,更加精确控制进程的生命周期。
– 在进入到有可能加载该进程的界面时,才去加载该进程。
– 在返回到先前界面,不可能再调用该进程时,退出进程。
32飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
进程启动速度
• 调整CPU频率
– 嵌入式设备中,CPU一般有几个工作频率。
– CPU频率越高,运行速度越快,耗电量越高。
– 我们可以在启动前调高CPU频率,在完成后再调低CPU频率。
– 其是以耗电量增加为代价,不推荐。
33飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
程序性能优化
• 性能优化≠effective C++ ≠ 高效代码
– Effective C++更加侧重于代码的良好的写作习
惯,日常代码的编写。
– 性能优化:不只是代码的优化,还包括程序逻辑的优化;对于代码,它更加看重对热点代码的优化。
– 高效的代码,对程序的性能有帮助,但并不意味着不需要优化。
34飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• 当你面对成千上万行代码时,你要做的寻找程序热点。
– 20%的代码占用了80%的时间。
– 如果我们花精力优化这20%的代码,将对程序的性能有很大的帮助。
• 如何查找程序热点:
– gprof
– Oprofile
35飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• gprof的基本用法:
– 使用 -pg 编译和链接你的应用程序,如果要得到带注释的源码清单,则需要增加-g选项。
– 执行你的应用程序,会在当前目录下产生gmon.out文件。
– 使用gprof 程序分析gmon.out文件,需要把它和产生它的应用程序关联起来:
gprof hello gmon.out –p 得到每个函数占用的执行时间。
• gprof的问题:
– 不支持动态库
– 不支持多线程
基本没有什么实用价值
36飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• Opofile的使用
# opcontrol –reset //重置Oprofile的统计数据
# opcontrol --no-vmlinux //不分析内核
# opcontrol -i /mnt/msc_int0/hello //指名进程的路径
# opcontrol -e CPU_CYCLES:5000:0:1:1 //设置跟踪事件
# opcontrol –start //开始采样
Using 2.6+ OProfile kernel interface.Using log file /var/lib/oprofile/oprofiled.logDaemon started.Profiler running.# ./hello
//运行进程
# opcontrol –shutdown //停止采样
Stopping profiling.Killing daemon.
37飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• # opreport –lsamples % symbol name2345696 99.6996 slow_multiply3968 0.1687 main3099 0.1317 fast_multiply
• # opannotate --source --base-dirs=/home/bookcode/ --search-dirs=/mnt/msc_int0/ ./hello
:int slow_multiply(x, y)/* slow_multiply total: 2345582 99.7027 */
: int i, j, z;1626169 69.1229 : for (i = 0, z = 0; i < x; i++)716317 30.4482 : z = z + y;1278 0.0543 : return z;123 0.0052 :}
38飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• 在嵌入式设备中使用oprofile– 嵌入式设备中由于flash有限,故动态库和进程往往是去除符号的。
– 由于flash有限,动态库和进程,不具备调试信息。
– 这直接导致了opreport –l,不能列出每个函数所占CPU的时间,也无法使用opannotate将代码与时间相对应。
如果这样,oprofile几乎对我们没有什么用途。
39飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• 我们可以采用在设备内采样,外部解析的方式。
– 生成符号表:#nm -n -C hello– 在设备上采样
# opcontrol --reset# opcontrol --no-vmlinux# opcontrol -e CPU_CYCLES:5000:0:1:1# opcontrol -i /mnt/msc_int0/hello# opcontrol --separate=lib# opcontrol –startUsing 2.6+ OProfile kernel interface.Using log file /var/lib/oprofile/oprofiled.logDaemon started.Profiler running.# ./hello# opcontrol --shutdownStopping profiling.Killing daemon.
40飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• 在设备上运行# opreport –d000003ec 394 0.0754
000003f0 124 0.0237
000003f4 136 0.0260
........
• 使用前面获得的符号表,对其进行解析,就可以知道每个函数所占用的时间。
• 还可以使用#addr2line -e ./hello 000003f0 ,来获得指令与代码的对应关系,前提是进程(动态库)编译时使用了-g选项。
41飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• Oprofile的局限
– oprofpp 不能够正确地归类内联函数样品 —oprofpp 使用一个简单的地址范围机制来决定它所
在的是哪个函数的地址。内联函数样品不从属于那个内联函数,而是从属于那个内联函数所插入的函数。
– 非CPU约束的性能问题 — OProfile 能够找出受CPU 约束的进程的问题。OProfile 不会识别正处
于睡眠状态的进程,因为这些进程正在等待锁或其它事件的发生(如等待 I/O 设备完成操作)。
42飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点• Oprofile实战
int funca(int a,int b) {int ret;ret=a*a*a/b;ret=ret/b;ret=ret/b;return ret;
}int ret;int funcb() {
int i=0;int sum;for(i=0;i<1024*1024;i++) {
ret+=funca(200,i);sum+=i;
}printf("sum=%d\n",sum);return 0;
}
int main() {funcb();sleep();return 0;
}
# opcontrol --reset# opcontrol --no-vmlinux# opcontrol -e CPU_CYCLES:5000:0:1:1# opcontrol --separate=lib# opcontrol -i /mnt/msc_int0/hello# opcontrol --startUsing 2.6+ OProfile kernel interface.Using log file /var/lib/oprofile/oprofiled.logDaemon started.Profiler running.# ./hellosum=-1091043676# opcontrol --shutdownStopping profiling.Killing daemon.
43飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• # opreport –lsamples % image name symbol name18872 66.6337 hello __aeabi_idiv6727 23.7519 hello funca2651 9.3602 hello funcb
• 从这个结果上看,我们应该着重优化funca。• 可实际上,funca并没有用途,我们应该去掉。
• 这个例子告诉我们,在我们知道某个函数是热点函数之后,不要急于去优化该函数,还要花很大的精力去了解为什么会跑到这个函数,这个程序的逻辑是否合理?
44飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• 当我们拿到每个函数占用时间的百分比之后。
– 先检测耗时前几名的函数,按照程序执行的逻辑来讲,是否合理,是否有存在的必要?
– 弄清楚排名前几名函数的调用关系。
• 比如:我们看到libc的memcpy函数占用了大量的内
存,但问题可能是,你的程序里为什么会做大量的memcpy,而不是去如何优化memcpy。
– 尝试去优化耗时前几名的函数。
45飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
查找程序热点
• 一个Case:
一个屏幕,中间一个区域变化。
实现方案:当中间区域变化时,修改对应的变量,整个屏幕重画。
• 你应该如何优化?
• Oprofile并不能直接帮助我们发现程序逻
辑上的问题。
46飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
程序性能优化
• 程序逻辑优化
一般我们通过代码的优化,可以将程序的性能提高几倍。可是如果我们在上层软件逻辑、算法上的优化,却可以将程序的性能提高十几倍。
– 先缕清楚程序运行过程中多做了哪些事情
– 程序在做这些事情的时候,都用了多少时间。
– 在我们知道了各部分所用的时间之后,就能很清楚的了解从逻辑层面上,性能的瓶颈在哪里。
– 这时我们就可以考虑如何优化程序的逻辑,哪些部分可以去掉、哪些部分可以优化?
47飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
程序性能优化
• 优化的层次
– 上层业务逻辑的优化
这更多的是面向具体的业务逻辑,我们可以通过打log的方
式,来获得软件各个逻辑功能模块所占用的时间,找到性能瓶颈,进行优化。
– 底层基础函数性能优化
这是一个打基础的过程,如果底层地基打不好,那么上层应用就会疲于应付KPI,不断的简化业务逻辑,甚至有些功
能不得不舍弃,这不是我们所期望的。
这时候,我们要使用oprofile,多跑一些case,发现底层热点
函数,着重去优化它。
48飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
程序性能优化
• 何时开始性能优化
– 在需求阶段,在提出用户需求的同时,也要把性能指标也要定义下来。
– 在软件设计阶段,要考虑这些性能指标,根据这些指标来考虑程序所使用的算法、逻辑,在这个阶段就要考虑逻辑上的优化。
– 在软件功能基本完成后,一方面软件的逻辑要做一些细微的调整,同时要开始使用oprofile来查找热点函数,对热点函数做代码优化。
49飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
程序性能优化
• 程序逻辑优化
– Do it faster。
– Do it in parallel
– Do it later
– Don't do it at all
– Do it before.
50飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
程序性能优化算法的优化
• 常用的算法优化
– 采用复杂度更低的排序算法
– 使用查表法取代计算密集型操作
– 考虑事件的特殊性
51飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
程序的优化
• 代码优化的境界
– 当你面向代码时,你能看到编译器对其优化后所产生的汇编指令。这样你就可以清楚,什么样的代码,产生的汇编指令运行的速度更高,也就清楚如何去优化它。
– 你要了解你所面向的芯片组,当你面向代码时,能够看到汇编语言在硬件中的执行状态。
这两个境界,为所有代码优化人员指明了前进的方向。
52飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
程序的优化
• GCC编译优化
• 标准C语言代码优化
• C++代码优化
• 硬件相关的优化
53飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
GCC编译优化
• 使用条件编译,替代某些平台相关的参数判断。
– 在生成的代码中,指令更加精简,速度更快。
• 使用built-in函数,来完成一些特殊的功能,例如实现SIMD指令。
• GCC编译优化选项:http://man.lupaworld.com/content/develop/GCC_zh.htm
• GCC与G++的不同
• 指令CPU的型号
– #gcc –o hello –mcpu=arm1136js –O2 hello.c
54飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 数据类型:int char short那种效率 高。
一种说法:
8位和16位的变量,在做完加法操作后,需要在32位的寄存器中进行符号扩展,其中带符号的变量,要用逻辑左移(LSL)接算术右移(ASR)两条指令才能完成符号扩展;无符号的变量,要使用一条逻辑与(AND)指令对符号位进行清零。
char a;
a=a+1
ADD a1,a1,#1
AND a1,a1,#&ff
short a;
a=a+1
ADD a1,a1,#1
MOV a1,a1,LSL #16
MOV a1,a1,ASR #16
int a;
a=a+1
ADD a1,a1,#1
55飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 代码优化方法与你所使用编译的版本、目标的体系结构高度相关。
• 这里所面向的平台:
– 编译器:gcc version 3.4.3
– 硬件:ARM11芯片组,ARM1136JF-S
– 编程语言:C和C++
56飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 实际上
char p=0;p++;mov r3, #0strsb r3, [fp, #-13]ldrsb r3, [fp, #-13]add r3, r3, #1strsb r3, [fp, #-13] 在ARM V4版本增加了半字的读取和写入指令;读取带符号的字节和半
字数据的指令。
故int char short的性能是一样的。
57飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 数组
– 数组寻址
int m[20][32];
ret=m[2][3]
注意:r0=2;r1=3;r3为二维数组m的起始地址
add r1, r1, r0, lsl #5 //r1=3+2*32=67
ldr r1, [r3, r1, lsl #2]
二维数组的寻址地址由原来的3条,减少到现在的2条。其主要原因在于二维数组的列数为2的幂次方。
int m[20][20];
ret=m[2][3]
注意:r0=2;r1=3,r2为二维数组m的起始地址;
add r3, r0, r0, lsl #2 // r3=2+2*4=10
add r3, r1, r3, lsl #2 //r3=3+10*4=43
ldr r1, [r2, r3, lsl #2] //因为二维数组m是int型,所以偏移还要乘以4
58飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 尽量使用一维数组
– 也就是我们上面看到的,一维数组在寻址时要比多维数组效率要高。
– 在数组遍历时,一维数组遍历要比多维数组遍历使用更少的指令,因而效率也更高。
59飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 结构83e4: e24dd008 sub sp, sp, #8 ; 0x8 //为s1申请栈空间
83e8: e59d2004 ldr r2, [sp, #4] //r2=s1.n2
83ec: e1a01000 mov r1, r0 //r1=n
83f0: e3a03009 mov r3, #9 ; 0x9 //i=9
83f4: e0820001 add r0, r2, r1 //r0=s1.n2+n
83f8: e2533001 subs r3, r3, #1 ; 0x1 //i--
83fc: e1a02000 mov r2, r0 //s1.n2=r0
8400: 5afffffb bpl 83f4 <func+0x10>
8404: e28dd008 add sp, sp, #8 ; 0x8
8408: e12fff1e bx lr
struct MyStruct {
int n1, n2;
};
int func(int n) {
struct MyStruct s1;
int ret,i;
for(i=0;i<10;i++)
s1.n2+=n;
ret=s1.n2;
return ret;
}
60飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 结构变量对程序性能的影响:
– 如果函数中存在结构变量,编译器将为其分配栈空间,即使还有空闲的寄存器,对程序的性能有负面作用。
– 函数中第一次访问结构成员变量时,会根据结构变量的首地址+偏移的方式,将结构成员变
量加载到寄存器中;以后再访问该变量时,直接操作寄存器就可以了,不会影响程序的性能。
61飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 变量
– 全局变量与静态变量性能比较
• 全局变量和静态变量其生命周期都是一样,与process的生命周期相同,都是存储在程序的数据段
中,只是其作用域范围不同。不过这种作用域范围,只是针对于编译器来讲,在编译器编译时,会检测相关的错误;但在编译后的汇编代码执行角度来讲,其都是从数据段加载,静态变量所对应的数据段数据,在程序范围内都是有效的。
• 性能是一样的。
62飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 栈变量与全局变量8430: e59fe034 ldr lr, [pc, #52] ; 846c //获取m的地址
8434: e59f0034 ldr r0, [pc, #52] ; 8470 //获取n的地址
8438: e59e3000 ldr r3, [lr] //r3=m
843c: e5902000 ldr r2, [r0] //r2=n
8440: e0833001 add r3, r3, r1 //m+=i
8444: e0822001 add r2, r2, r1 //n+=i
8448: e2811001 add r1, r1, #1 ; 0x1
844c: e151000c cmp r1, ip //比较i是否<a
8450: e58e3000 str r3, [lr]//将m的数值回写到数据段
8454: e5802000 str r2, [r0]//将n的数值回写到数据
段。
8458: bafffff6 blt 8438 <func+0x24>
int m=5;
int func(int a){
int ret,i;
static int n=7;
for(i=0;i<a;i++) {
n+=i;
n+=i;
}
……..
}
63飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 栈变量与全局变量
int m=5;
int func(int a){
int ret,i;
static int n=7;
int j=m;
for(i=0;i<a;i++) {
j+=i;
n+=i;
}
m=j; ret =n+m;
return ret; }
int m=5;
int func(int a){
int ret,i;
static int n=7;
for(i=0;i<a;i++) {
m+=i;
n+=i;
}
ret =n+m;
return ret;
}
优化后优化前
64飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化• 变量作用域与栈顶指针的变化
int c1,c2;
for(i=0;i<10;i++) {
c1=0;
c2=1;
printf("%d %d\n",c1,c2);
}
for(i=0;i<10;i++)
{
int c1=0;
int c2=1;
printf("%d %d\n",c1,c2);
}
ldr r0, [pc, #36] ; 84d0 <.text+0x164> //获取字符串”%d %d\n"的地址。
add r5, r5, #1 ; 0x1 //r5对应与变量i
mov r2, #0 ; 0x0 //r2对应变量c1
mov r3, #1 ; 0x1 //r3对应变量c2
bl 8360 <__plt_header+0x38>//调用函数printf
cmp r5, #9 ; 0x9
65飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 变量作用域与栈顶指针变化
8418: e1a02000 mov r2, r0 //r0对应于变量m
841c: e3520000 cmp r2, #0 ; 0x0
8420: e59f0024 ldr r0, [pc, #36] ; //r0对应”%d \n”字符产的地址
8424: 059f0024 ldreq r0, [pc, #36] ; //如果m为0,r0加载”%d 1\n”字符串的地址
8428: e24dde4b sub sp, sp, #1200 ; //为a[100]和b[200]开辟栈空间
842c: e24dd004 sub sp, sp, #4 ; 0x4
8430: 159d1320 ldrne r1, [sp, #800]//if m!=0 r1=a[0]
8434: 059d1000 ldreq r1, [sp] //if m==0 r1=b[0]
8438: ebffffc8 bl 8360 <__plt_header+0x38> //printf
843c: e3a00000 mov r0, #0 ; 0x0 //返回值0
int test(int m)
{
if(m) {
int a[100];
printf("%d\n",a[0]);
}else {
int b[200];
printf("%d 1\n",b[0]);
}
return 0;
}
66飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 变量作用域与栈顶指针变化
– 在进入函数分配栈空间的时候,程序并不是按照执行分支的不同,不断的修改栈顶地址,而是在进入函数的时候,为所有的变量所占用的空间求和,一次性配置栈地址,不管这时变量是否有效。这样的好处在于GCC能够很方便
的知道各个变量在栈中的位置,不会导致栈因为程序执行路径的不同,变量位置不同所带来的定位的复杂性。这样所带来的后果便是,栈将会使用大量的内存。
68飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 除法
– 在ARM指令集中没有除法指令,其除法是通过C库函数实现的。根据执行情况和输入操作数的范围,一个32位的除法通常需要20~140个时钟周期。
– 尽量少用除法
– 用减法代替除法
– 当除数为2的幂次方时,使用移位操作代替除法
– 利用与运算代替求余运算
– 合并除法
– 用乘法代替除法
– 近似计算除法 (该替代方法不等价) – 用查表的方法代替除法
69飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 浮点
– 如果没有浮点协处理器,我们可以使用软浮点
• #gcc test.c –msoft-float –o test
– 如果系统有浮点协处理器,我们可以使用硬浮点。
• #gcc -o hfloat –mcpu=arm1136js f1.c (gcc缺省支持硬浮点)
注意,这里我们还要告诉gcc CPU的型号,以便生成对应的指令。
– arm11jf-s使用的是VFP11协处理器,除了支持浮点运算的:单精度、双精度浮点数加、减、乘、除外,还支持乘方、平方根、绝对值、浮点取整、整数转换到浮点,单精度和双精度浮点之间的转换。
70飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 浮点运算优化:
– 将浮点运算转换为乘除运算。
– 优先使用浮点处理器;如果系统不支持浮点处理器,使用glibc提供的软浮点。
– 对于计算密集型的算法,我们可以通过转化为查表来进行优化。
– 在保证计算精度的前提下,确定浮点型变量和表达式是 float 型
71飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• if语句
代码的性能是一样的。
e3540000 cmp r4, #0 ; 0x0
02855004 addeq r5, r5, #2 ; 0x2
e3540000 cmp r4, #0 ; 0x0
12855002 addne r5, r5, #2 ; 0x2
if(!c) ret+=2; if(c) ret+=2;
code2code1
72飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• if连接表达式
– 从左到右对表达式求值
– 当结果确定后,不再有其他需要计算的表达式,也就是俗称的“短路”。
• 优化方法:
– 删除冗余条件 例如:if(a>0&&a<0x666&&a!=0)– 删除肯定不成立的条件 if(a!=0&&a==0)– 利用短路机制,将计算速度 快的表达式放在左
边。
• if((strlen(a)>100) || (b>100))
73飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• if语句并不一定导致分支语句
84c8: e3540000 cmp r4, #0 ; 0x0 // if(b)
84cc: e2833001 add r3, r3, #1 ; 0x1
84d0: 12866002 addne r6, r6, #2 ;//if(b!=0) ret+=2
84d4: 12855001 addne r5, r5, #1 ;//if(b!=0) ret1+=1
84d8: 02866003 addeq r6, r6, #3 ;//if(b==0) ret+=3
84dc: 02855002 addeq r5, r5, #2 ; //if(b==0) ret1+=2
if(b)
{
ret+=2;
ret1+=1;
}else
{
ret+=3;
ret1+=2;
}
74飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• switch语句
– 在case值变化不大的时候,并且有足够的分支(大于4个分支)的时候,将采用跳转表的方式进行优化,default值的效率 高。
– 在case值变化比较大的时候,将采用二叉比
较的方式进行优化,减少判断的深度,这时取值中间的case值,效率 高。
75飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 循环语句,优化的重点
– 将不变的代码移到循环之外
– 将分支语句提到循环的外面
if(n==1) {
for(i=nloop;i>=0;i--)
j+=2;
}else {
for(i=nloop;i>=0;i--)
j+=1;
}
for(i=nloop;i>=0;i--)
if(n==1)
j+=2;
else
j+=1;
76飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 循环语句优化
– 循环的展开,通过循环分支的展开,我们可以降低循环的次数,从而减少分支语句对循环的影响。
for(n = 0; n <1024*256 ;n++)
{
n1++;
n1++;
n1++;
n1++;
}
31708 ms
for(n = 0; n <1024*512 ;n++)
{
n1++;
n1++;
}
41505 ms
for(n = 0; n <1024*1024 ;n++)
{
n1++;
}
103576 ms
77飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 循环语句优化
– 循环的合并
– 用减1指令替换循环加1指令
for(b=0;b<10;b++) {
m[b]=b;
n[b]=b;
}
for(b=0;b<10;b++)
m[b]=b;
for(b=0;b<10;b++)
n[b]=b;
8450: e2533001 subs r3, r3, #1 ; 0x1
8454: e2811002 add r1, r1, #2 ; 0x2
8458: 5afffffc bpl 8450
842c: e2833001 add r3, r3, #1 ; 0x1
8430: e1530004 cmp r3, r4
8434: e2811002 add r1, r1, #2 ; 0x2
8438: dafffffb ble 842c
for(i=nloop;i>=0;i--) j+=2;for(i=0;i<=nloop;i++) j+=2;
78飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 函数
– 内联函数
• 为了调试方便,当函数在编译时,没有采用-O指定优化级别,内联函数不做展开,除非用户使用always_inline,来声明函数。
• 编译器展开内联函数的前提是,在同一个编译单元能够找到该函数的定义,否则就不能展开内联函数。因此,内联函数的声明和定义一般都包含在头文件中。
• inline修饰符并非强制性的,编译器有可能会对它置之不理。例如,递归函数通常不会被编译成inline函数,编译器有权自行决定是否要将定义成inline的函数编译成inline。
• 展开inline函数并不一定会导致代码量增大。因为在函数调用时,需要使用一些指令来安排进出函数时,栈结构的变化;而如果inline函数展开,则省去了这些指令。如果inline函数体的代码量小于安排栈帧的代码量,那么将函数标识为inline,反而会使代码量减小。
• GCC在编译时,如果使用了-O3优化编译开关,其会将一些代码量小的函数转变成inline来处理,即使这个函数没有使用inline标识符。
• 在C++中,在类的内部定义了函数体的函数,被默认为是内联函数。而不管你是否有inline关键字。
79飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 函数
– 纯函数
是指不会影响它自己范围外任何事情的函数。它可以读取全局变量或者通过指针传递的变量,但是不可以对这些变量进行些操作。它也不可以读取volatile变量和外部资源(比如文件)。
– 纯函数
是纯函数的更严格版本。它不会读取和写除参数外的任何数据,也不可以使用指针参数来读取数据。“函数的结果只依赖参数”。
int func(int a, int b) __attribute__ (pure);
80飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 寄存器的使用,遵从ATPCS标准。
• ATPCS标准:
– 子程序间通过寄存器R0~R3来传递参数。被调用的子程序在返回前无需恢复寄存器R0~R3的内容。
– 在子程序中,使用寄存器R4~R11来保存局部变量。如果在子程序中使用了寄存器R4~R11中的某些寄存器,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值;对于子程序中没有用到的寄存器则不必进行这些操作。
– R12用作子程序间scratch寄存器,记作ip。在子程序间的连接代码段中经常使用这种规则。
– R13用作数据栈指针,记作sp。在子程序中寄存器R13不能用作其他用途。
– R14称为连接寄存器,记作lr。它用来保存子程序的返回地址。
– R15是程序计数器,记作pc。– 子程序返回结果为一个32位整数时,可以通过寄存器R0返回;结果为一个64
位整数时,可以通过寄存器R0和R1返回,依次类推。
81飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 函数的参数 好不多于4个– 4个以下的形参可以通过寄存器来传递,4个以上的参数,则要通过栈来传递。
– 同时如果参数小于4个,则r0~r4中剩余的寄存器可以用来保存函数中的局部变量。
• 减少局部变量的个数
– 尽量限制函数内部循环所用局部变量的数目, 多不超过12个,以便编译器能把变量分配到寄存器。
– 如果没有局部变量保存到栈中,系统也将不必设置和恢复栈指针。
• 当函数内部寄存器变量多于12个时,并不意味着只是将前面的12个临时变量分配寄存器,之后的临时变量都是通过栈内存来操作。
– 当寄存器分配完后,遇到新的临时变量时:
– 查看已分配寄存器的局部变量,是否有在后面的代码中不会再被使用,则新的局部变量使用其所占用的寄存器。
– 如果已分配寄存器的局部变量,在后面的代码中都要使用,则要选择出一个临时变量,将其保存到栈中;将其所使用的寄存器分配给局部变量。
82飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 文件操作
– 读写文件,缓冲区的buffer为4096 2048时 快。
– 利用mmap,读写文件。
利用mmap实现的一个文件拷贝例子
其基本流程是
• 1、创建一个与源文件大小相同的目标文件;
• 2、使用mmap,分别将源文件和目标文件映射到内存中。
• 3、使用memcpy,将文件读写操作,转换成内存的拷贝操
作。
83飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
标准C代码优化
• 线程
– 创建线程是有代价的,如果只让线程做很少的事,而又频繁的创建和销毁线程,是得不偿失的。
– 使用异步IO,来取代多线程+同步IO的方式。
– 使用线程池,取代线程的创建和销毁。
84飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 构造函数与析构函数
• 对象的作用域
• 访问成员变量
• 成员函数
• 全局对象与静态对象
• 栈对象与堆对象
85飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 构造函数与析构函数
Myclass obj;– 1、在函数的栈中,为该对象分配一片内存,能够容纳
其所有的类成员。
– 2、如果其有基类,先运行基类的构造函数。
– 3、如果类的成员变量为类对象,那么还要运行类成员变量的构造函数。
• 步骤2和3,视类的继承层次是一个递归的过程。总的来讲,先构造基类,然后再构造父类。
• 小心使用对象数组,将为每个数组成员运行构造函数。
86飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 在声明对象时进行初始化
– 例如: Myclass obj = data;
• 构造函数初始化列表
class c1: public Base {public:c1():i(10) {………………….}private:
int i;}
• 内联构造函数和析构函数
87飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 对象的作用域
class Myclass;
………
for(int i=0;i<10;i++)
{
Myclass obj;
……..
}
for(int i=0;i<10;i++)
{
int j;
……..
}
非内置类型内置类型
88飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化• 访问成员变量
8434: e52de004 str lr, [sp, #-4]!
8438: e24dd00c sub sp, sp, #12 ; 0xc//为对象obj分配栈空间。
843c: e1a0000d mov r0, sp //r0为对象的起始地址
8440: ebfffff6 bl 8420 //有了对象的起始地址,开始运行对象的构造函数
8444: e59d0004 ldr r0, [sp, #4] //对象首地址偏移4,就是成员变量m的地址,ret=obj.m。
8448: e28dd00c add sp, sp, #12 ; 0xc
844c: e8bd8000 ldmia sp!, {pc}
class Myclass{
public:
int n,m;
Myclass();
};
Myclass::Myclass(){
n=1; m=2;
}
int func(){
Myclass obj;
int ret=obj.m;
return ret;
}
89飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 访问成员变量
– 第一次是通过对象首地址加偏移的方式加载到寄存器中,以后对该成员变量的访问,都将在寄存器中执行。
– 公有变量与私有变量,只是编译器层的概念,执行的效率是一样的。
– 静态成员变量,存储在数据段,其性质与全局变量一样,在做循环时会频繁的读写数据段内存。
90飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 成员变量
– 隐含this指针
– 将值传递改为引用传递
• 比如:
• int func(Object a);• 改为
• int func(const Object &a);
int Myclass::sum(this){
int ret=this->m+this->a;
return ret;
}
int Myclass::sum(){
int ret=m+a;
return ret;
}
91飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 静态成员函数没有this指针。
• 虚函数在运行时动态绑定,效率要比其他函数低。
92飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 全局对象
– 在动态库加载时,运行其构造函数,创建全局对象。
• 函数内的静态对象
– 当第一次运行到静态对象声明语句时,运行其构造函数。那么是如何区分第一次的呢?
int func() {static Myclass obj;obj.n++;return obj.n ;
}在BSS区,为obj分配内存外,还分配了一块4字节的内存来指示obj是
否被创建。
93飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 在func函数中,其运行流程如下:
– 首先检测_ZGVZ4funcvE3obj是否为1。– 如果为1,说明静态对象obj已经创建,则不必运行其构
造函数。
– 如果为0,说明静态对象obj没有创建,则运行其构造函数,并在创建完对象后将_ZGVZ4funcvE3obj置为1。
• 因此全局对象与函数内的静态对象性能上的差别在于:
– 全局对象是在进程启动时创建;函数内的静态对象是在第一次运行时创建。
– 在代码中使用全局对象时,可以直接使用全局对象;
在代码中使用函数内的静态对象时,需要先判断该全局静态对象是否被创建。
94飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
C++代码优化
• 栈对象与堆对象
• 栈是一片连续内存,因此一般其都已分配物理页面。
• 堆,即使紧挨着的两块内存分配,其内存地址也不是相连的,很可能导致page fault。
class Myclass;
void func(){
Myclass *pobj=new Myclass;
………
}
class Myclass;
void func() {
Myclass obj;
…….
}
95飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• 想了解指令是如何在系统内执行的,那么 好的文档就是其体系结构。
– ARM11微结构的流水线与以前的ARM核不同,它包含8级流水,使贯通率比以前的核提高40%。
– 单指令发射 : ARM11微结构的流水线是标量的(SCALAR),即每次只发射一条指令(单发射)。有些流水线结构可以同时发射多条指令,例如,可以同时向ALU和MAC流水线发射指令。理论上,多发射微结构会有更高的效能,但实践上,多发射微结构无疑会增加前段指令译码级的复杂程度,因为需要更多的逻辑来处理指令相关(DEPENDENCY),这将使处理器的面积和功耗变得更大。
– 分支预测:ARM11微结构使用两种技术来预测分支。首先,动态的预测器使用历史记录来判断分支是 频繁发生,还是 不频繁发生。如果动态的分支预测器没有发现记录,就使用静态的分支算法。很简单,静态预测检查分支是向前跳转还是向后跳转。假如是向后跳转,就假定它是一个循环,预测该分支发生,假如是向前跳转,就预测该分支不发生。通过使用动态和静态的分支预测,ARM11微结构中分支指令中的85%被正确预测。
96飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• ARM V6体系结构性能:
– 并行流水线:尽管流水线是单发射的,在流水线的后端还是使用了三个并行部件结构,ALU,MAC (乘加),LS(存取)。
– 64位数据路径:ARM11 微结构在局部合理使用64位结构,通过32位的成本来实现64位的性能。ARM11微结构在处理器整数部件与缓存之间,整数部件与协处理器之间使用了64位数据总线。64位的路径可以在一个周期内从缓存中读取两条指令,允许每周期传送两个ARM寄存器的数据。这使得许多数据移动操作与数据加工操作变得更为高性能。
– 浮点处理:使用了浮点寄存器
98飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• 流水线的优化
– 减少分支和跳转指令
– 消除数据的相关性
– 内存与计算相结合
1 a=a+1;
2 c=c+1;
3 b=a+d;
1 a=a+1;
2 b=a+d;
3 c=c+1;
1 a1=A[1];
2 c++;
3 d++;
4 a2=A[2];
a1=A[1];
a2=A[2];
c++;
d++;
99飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• 内存访问:
– 目前嵌入式设备所使用的内存一般为SDRAM。
100飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• 内存访问流程
– 1、CPU试图访问一块内存;
– 2、CPU首先该内存是否已经被加载到Cache中。
– 3、如果加载到cache中,则直接在cache中定位。
– 4、如果未加载到cache中,则通过CPU和内存之间的地址总线,向内存发送地址的高27位地址。
– 5、当内存收到高27位地址后,利用SDRAM的突发交换模式(burst-exchange mode),将连续的32个字节,传送给CPU上的Cache,填充一个缓存行。
– 6、CPU可以通过地址的高27位来定位Cache的缓存行,利用地址的低5位定位到缓存行中具体的字节。
101飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• 内存操作的优化
– 尽量使用占内存少的算法
– 利用流水线内存存取与计算并行的特点,组合内存访问与计算。
for(int i=0;i<1024;i++)
{
c=p[i];
a+=1;
b+=i;
}
for(int i=0;i<1024;i++)
{
a+=1;
b+=i;
c=p[i];
}
102飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• Cache的优化
– 提高运行指令和数据的局部性。
– 循环的变换
for(i=0;i<1024;i++)
{
for(j=0;j<128;j++)
{
ret += b[i][j];
}
}
for(i=0;i<128;i++)
{
for(j=0;j<1024;j++)
{
ret += b[j][i];
}
}
103飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化• Cache的优化
– 优化数据结构PhoneBook * FindName(charLast[],PhoneBook * pHead)
{
while(pHead != NULL)
{
if(stricmp(Last,pHead->LastName)==0)
return pHead;
pHead = pHead->pNext;
}
return NULL;
}
typedef struct _PHONE_BOOK_ENTRY {
char LastName[16];
char FirstName[16];
char email[16];
char phone[10];
char cell[10];
char addrl[16];
char addr2[16];
char stat[2];
char zip[5];
_PHONE_BOOK_ENTRY *pNext;
} PhoneBook;
104飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• SIMD指令
– ARM V6支持简单的SIMD指令。
– ARM V7提供了64/128位单指令多数据流(SIMD)指令集,用于
新一代媒体和信号处理应用加速。
– 指令集
105飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• SIMD指令使用:
– 使用汇编语言,我们可以使用内联汇编的方式,在C语言中嵌入汇编指令。
– 使用GCC编译器对于某些特殊指令提供了两种方法进行支持:Intrinsics和built-in函数。
intrinsic、built-in在C/C++程序中的语法是以函数形式出现, 编译时可以直接翻译为一条SIMD指令(复合情况会生成 直接的几条),换言之,如果不使用intrinsic,可能需要多条C/C++语句完成,而编译器却并不能保证将这几条语句能够生成这条
高效的SIMD指令。并不是每条SIMD指令都有对等的intrinsic,这需要查看GCC相应的手册。
106飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
硬件相关的优化
• 内联汇编的一个例子:
static int addsub(int x,int y){
__asm__(
"qaddsubx %0 , %1 , %0;": "=r" (x): "r"(x),"r"(y)
) ;return x;
}
107飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
系统级优化
• 调整进程的优先级
– Linux支持两种进程:实时进程和普通进程
– 实时进程的优先级是静态设定的,而且始终大于普通进程的优先级。对于实时进程来讲,其使用绝对优先级的概念,绝对优先级取值范围是0~99,数字越大,优先级别越高。
– 普通进程的绝对优先级取值是0 。在普通进程之间,其又具备静态优先级和动态优先级之分。静态优先级,我们可以通过程序来进行修改。同时系统在运行过程中,会在静态优先级基础上,不断的动态计算出每个进程的动态优先级,拥有 高动态优先级的进程被调度器选中。一般来讲,静态优先级越高,进程所能分配的时间片越长 。
108飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
系统性能优化
• 实时进程设置
#include <sched.h>int sched_setscheduler (pid t pid, int policy, const struct sched param *param);
这个函数可以同时设置一个进程的静态优先级和调度策略。pid用来指名所要设置的进程号,如果pid为0,则表示为当前进程;policy设置进程的调度策略;param用来设置进程的绝对优先级。
参数policy 取值范围:
SCHE_OTHER 普通进程
SCHE_FIFO 实时进程,采用先进先出策略。
SCHE_RR 实时进程,采用轮转策略
参数 paramstruct sched_param {
int sched_priority;};sched_priority为进程的绝对优先级,其取值范围0~99。返回值:如果设置成功,返回值为0;如果失败,返回指为-1。
109飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
系统性能优化
• 普通进程优先级调整
#include <sys/resource.h>int setpriority (int class, int id, int niceval);class的取值为:
PRIO_PROCESS id为进程的pid。PRIO_PGRP id为进程所在组的组ID。
PRIO_USER id为用户IDniceval 为进程的nice值,其取值范围为-20~19。返回值 成功返回0;失败返回-1。
另外一个常用的函数为int nice (int increment);increment为所设置进程nice值的增量。
110飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
系统性能优化
• 让进程慢下来
– 对于某些进程,我们对其没有时限要求,只需要它在背后默默做事,尽量不影响其他进程运行的进程,它不着急完成任务,但要求其使用尽可能少的CPU。
– 可以考虑降低进程的优先级
– 在计算密集型区域,增加调度函数。sleep(0)、sched_yield()。
for(i=0;i<1000000;i++){
j+=i;sleep(0);
}
111飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
系统性能优化
• 设备启动速度
– 一个不错的网址:http://tree.celinuxforum.org/CelfPubWiki/BootupTimeResources
– 尽量不要把某些进程放到启动脚本中,尝试daemon进程在第一次使用时启动。
112飞翔 Email:[email protected] blog:http://blog.chinaunix.net/u/30686/
系统性能优化
• 系统的耗电量
– 对于软件来讲,CPU的主频越低,耗电量也越
低。
– 调整CPU主频的两个方法:
• 使用接口函数
• Daemon进程根据CPU的负载,自动调整CPU的频
率。