原 数值计算优化方法C/C++
C++编程优化——让你的代码飞起来 RGB格式的彩色图像先转换成黑白图像
C/C++代码优化具体方案
1.关于继承:尽量少使用多重继承
不可否认良好的抽象设计可以让程序更清晰,代码更看起来更好,但是她也是有损失的,在继承体系中子类的创建会调用父类的构造函数,
销毁时会调用父类的析构函数,这种消耗会随着继承的深度直线上升,所以不要过度的抽象和继承,
更为严重的是当多重继承中并且有虚函数的存在时情况更为复杂,的确,这些问题涉及开销,但是多重继承减少了编码的负担,
同时也让问题的解决方案更加简洁,这当然要付出一些代价。总之,与n个基类的多重继承层次相关的额外虚函数表有n-1个。
派生类和最左边的非虚基类共享同一个虚函数表。因此,带有2个基类的多重继承层次,
有1个(2-1=1)基类的虚函数表和1个派生类的虚函数表(最左边的基类与派生类共享该虚函数表),
总共有2个虚函数表,如果有虚继承的存在,会进一步增长这个过程,它是有额外的开销的。
2.对象的复合:
对象的复合和继承很相似,当一个对象包含其他对象构造时也会引起额外的构造。
关于这点可能会有很多人不解,认为这是不可避免的,举个例子,比如你的类A中包含了类B非指针和引用对象,
那么在你构造对象a的时候会自动调用b的无参构造函数,即使你还没有用到她,用指针代替就没有这种消耗,
另外如果你的一个对象中用到数组和字符串,你是选择string和vector还是char* 和 c系的数组呢,
如果没有用到c++stl库提供的相关的高级用法,建议选择后者。
3.构造函数:
尽量用 参数列表 初始化 代替 参数,避免值传递初始化。
4.变量延时定义:
从c系转过来的仍保留着c的习惯,在函数第一行先把所有用到的变量都定义好,
但是c是没有运行时的消耗的,对于c++时不一样的,对于c++对象的构造和销毁时有消耗的,
如果有大量的对象只在某个if条件的一个分支中出现,那就会有50%的情况这些消耗是可以避免的。
对于这点在一个类中也是一样的,如果成员中有成员只在某个时刻能用,就用指针代替,
在构造对象时初始化成空指针,避免构造时调用他的构造函数。
5.虚函数:
虚函数的底层实现是通过一个 虚函数表 来实现的,因此有虚函数的类构造时必须先初始化虚函数表,
函数调用时也必须先找到虚函数表,然后通过指针偏移找到相应的函数,通常情况下调用虚函数是没有运行时消耗的,
但是根据编译器的实现不同,在调用虚函数时,有些调用可能导致增加虚函数表大小的额外开销,
或者只有那些需要调整 this指针 的调用才会发生额外的运行开销,但不会增加虚函数表的大小,
在多重继承 和 虚基类的时候这种消耗会显著增加,关于继承已经提过,所以避免滥用虚函数和虚继承,
有时候可以用模版设计来代替虚继承,把运行时的消耗提前到编译期。
虚函数表
6.返回值优化:
虽然c++编译器会选择性的进行 RVO(return value optimization) 优化,
但是不是强制的,当函数有多个返回语句并且返回不通名称的对象,
函数过于复杂,返回对象没有定义拷贝构造函数时,rvo优化是不会执行的,
所以当函数返回一个很大的对象时在不确定rvo优化会执行时,尽量避免值传递。
7.变量的定义:在定义变量时 尽量避免 类型的不匹配 造成临时变量的产生。
8.内存管理:内存池
c++内存管理的大权由我们自己掌握,对于项目中要 频繁申请和释放的对象 建议用简单的内存池来管理,
可以大大的降低频繁申请和释放内存带来的消耗。
9.善用内联:
内联函数不仅仅是简单的函数调用似的优化,他还有一个最大的优点就是,
可以让编译期进行进行边界代码的运行环境优化,
内联把代码拷贝到执行环境处避免了函数调用带来的消耗,
并且编译期可以进行正常的编译优化,而函数调用是不能实现的。
10.stl :
记住一点stl不是唯一的选择,有时候也不是最好的选择,合理选择stl善用stl算法。
11 缓存:对于多次使用的计算结果及时缓存,避免重复计算。
12 延时计算:对于不关心计算结果的计算过程尽量延时执行或者异步去执行。
13 多线程:无锁化编程
尽可能的使用无锁式多线程开发,锁是一个非常消耗性能的东西,
保证数据同步的手段有很多,voalite,原子操作都可已实现,
尽量通过一些技巧使用这些手段避免锁 的使用,如果迫不得已要使用锁,
尽量减少锁的消耗,比如降低锁的粒度,使用性能更高的锁等等。
线程池 实现代码
算法优化之c++多线程优化:思考与总结
14 std::move操作:
当不得不进行 深拷贝时,如果 深拷贝数据源 在拷贝后就不在使用,尽可能的用move操作代替,
或者在参数传递时 用move操作代替 临时的 实参变量。
15 cpu缓存:合理的利用cpu cache缓存 可以极大的提高代码的运行效率(
例如:数组中以 每列遍历 和 每行遍历的效率的不同),
当然多线程环境下也要考虑cpu cache带来的影响。
16 内存对齐:
在进行网络编程时,最好对网络中传送的数据快进行内存补齐,
通常是8字节对其,提高cpu访问内存效率,从而提高数据读写速度。
17 函数参数:
用const引用 代替 值传递,如果函数参数过多,
可以用对象 来 打包参数,减少参数过多带来的性能消耗。
18 算法: 尽可能的优化你的算法。
19 关于智能指针:必须用
对于智能指针我的选择是 必须用,它可以大大降低程序的crash频率,
但是智能指针的和普通指针相比是有额外的消耗的,
她的底层是一个 原子操作 来 统计引用数 和 一个普通指针,
虽然原子操作和锁相比性能高了不少但是和普通的加减操作还是慢了不少,
智能指针 的大小为16个字节,而 普通指针 的大小只有4个字节,
拷贝的成本也不一样,所以在使用正确的情况下可以使用 智能指针的引用 来减少拷贝的消耗(
注意这里的前提是正确的使用引用,不要引用以一个即将被销毁的变量)。
20 内存池:
对于需要 频繁申请和释放 的内存对象,如果可以重复利用对象的内存,
强烈建议通过 内存池 或者 重载对象的 new操作符 或者 重载对象的 placement new操作符
来减少频繁的申请和释放内存,从而减少申请和释放内存的消耗和内存碎片的产生。
21 其他优化方案:位运算代替乘除法,前缀运算符代替后缀运算等等。
并行优化是代码优化的基本方法,从大到小一共可以分成三级:
异步框架;任务并行;数据并行。
在实际工作中,
第一步一般是先设计 异步框架,包括 异步处理任务 以及 异步任务 的 异构化 等;
第二步一般是做 数据并行优化(SIMD),利用CPU的 向量指令 来对 多条数据并行处理;
这两步是 代码 优化的重心,一般做完这两步,系统性能会有明显的提升。
今天要讨论的是第三步,for循环的并行优化。
与前两者不同的是,for循环往往是处理同一类任务,且通常会涉及到对同一个变量的读写,
所以异步是不能用,而且for循环中往往包含 多种结构 比如 逻辑判断 和 算术过程等,
所以通常也很 难用数据并行的方式来优化,那么怎么对for循环进行优化呢?
// 例1 内存操作,申请一段内存空间然后释放
void testfuc(int num){
int a = 0;
for (int i = 0; i != num; i++) {
int *b = new int[10]();
delete [] b;
}
假设num = 1e7;一千万次内存操作在我的机器上(2.6 GHz Intel Core i5双核)运行耗时在900ms左右。
为了对这个for循环进行优化,首先将for循环拆分成若干部分,比如两部分:
void testfuc(int num){
int a = 0;
int num2 = num >> 1;
// 前半部分
for (int i = 0; i != num2; i++) {
int *b = new int[10]();
delete [] b;
}
// 后半部分
for (int i = num2; i != num; i++) {
int *b = new int[10]();
delete [] b;
}
}
然后使用c11的 future + async来启动两个异步任务分别处理一个子循环:
// https://blog.csdn.net/u011726005/article/details/78266706
//--------------------------------------------------------------------------------
// 3.std::future可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。
// std::future通常由某个Provider创建,你可以把Provider想象成一个异步任务的提供者,
// Provider在某个线程中设置共享状态的值,与该共享状态相关联的std::future对象调用get(通常在另外一个线程中获取该值,
// 如果共享状态的标志不为ready,则调用std::future::get会阻塞当前的调用者,直到Provider设置了共享状态的值
//(此时共享状态的标志变为ready),std::future::get返回异步任务的值或异常(如果发生了异常)。
// 一个有效的std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联,分别是
// std::async 函数,std::promise::get_future。std::packaged_task::get_future。
// 在一个有效的 future 对象上调用get会阻塞当前的调用者,直到Provider设置了共享状态的值或异常。
// 4.c++11还提供了异步接口std::async,通过这个异步接口可以很方便的获取线程函数的执行结果。
// std::async会自动创建一个线程去调用线程函数,它返回一个std::future,这个future中存储了线程函数返回的结果,
// 当我们需要线程函数的结果时,直接从future中获取。
void testfuc2(int num){
int a = 0;
int num2 = num/2;
future<void> ft1 = async(std::launch::async, [&]{ // 封装为lambda函数 后传入
for (int i = 0; i != num2; i++) {
int *b = new int[10]();
delete [] b;
}
});
future<void> ft2 = async(std::launch::async, [&]{ // 封装为lambda函数 后传入
for (int i = num2; i != num; i++) {
int *b = new int[10]();
delete [] b;
}
});
ft1.wait();
ft2.wait();
}
将testfunc1和testfunc2放在一起测试,运行结果如下:
time1 = 992.360000
time2 = 475.182000
显然testfunc2要明显比testfunc1快,在本次运行结果中,时间少了一半,
但是这个时间不一定每次都是一半,由于线程切换和CPU状态的影响,
testfunc2的时间会比testfunc1节省40%-50%。
// 复杂计算 优化后函数为:
void testfuc2(int num){
int num2 = num/2;
future<void> ft1 = async(std::launch::async, [&]{
for (int i = 0; i != num2; i++) {
b = cos(tan(i));
}
});
future<void> ft2 = async(std::launch::async, [&]{
for (int j = num2; j != num; j++) {
c = cos(tan(j));
}
});
ft1.wait();
ft2.wait();
}
运行结果为:
time1 = 806.438000
time2 = 407.875000
1.引言
计算机技术和信息技术的高速发展的今天,计算机和计算机技术大量应用在人们的日常生活中,
嵌入式计算机也得到了广泛的应用。
嵌入式计算机是指完成一种或多种特定功能的计算机系统,是软硬件的紧密结合体。
具有软件代码小、高度自动化、响应速度快等特点。
特别适合于要求实时和多任务的应用体系。嵌入式实时系统是目前蓬勃发展的行业之一。
但是,实时嵌入式系统的特点使得其软件受时间和空间的严格限制,
加上运行环境复杂,使得嵌入式系统软件的开发变得异常困难。
为了设计一个满足功能、性能和死线要求的系统,
为了开发出安全可靠的高性能嵌入式系统,开发语言的选择十分重要。
2.嵌入式实时程序设计中语言的选择
随着嵌入式系统应用范围的不断扩大和
嵌入式实时操作系统RTOS(Real Time Operating System)的广泛使用,
高级语言编程已是嵌入式系统设计的必然趋势。
因为汇编语言和 具体的微处理器 的硬件结构密切相关,移植性较差,既不宜在复杂系统中使用,又不便于实现软件重用;
而高级语言具有良好的通用性和丰富的软件支持,便于推广、易于维护,因此高级语言编程具有许多优势。
目前,在嵌入式系统开发过程中使用的语言种类很多,但仅有少数几种语言得到了比较广泛的应用。
其中C和C++是应用最广泛的。C++ 在支持现代软件工程、 OOP(Object Oriented Programming,面向对象的程序设计)、
结构化等方面对C进行了卓有成效的改进,但在程序代码容量、执行速度、 程序复杂程度等方面比C语言程序性能差一些。
由于C语言既有低级语言的直接控制硬件的能力,又有高级语言的灵活性,是目前在嵌入式系统中应用最广泛的编程语言。
随着网络技术和嵌入式技术的不断发展,Java的应用也得到广泛应用。
3.C/C++代码在实时程序设计中的优化
虽然使软件正确是一个工程合乎逻辑的最后一个步骤,但是在嵌入式的系统开发中,情况并不总是这样的。
出于对低价产品的需求, 硬件的设计者需要提供刚好足够的存储器和完成工作的处理能力。
所以在嵌入式软件设计的最后一个阶段则变成了对代码的优化。
现代的C和C++编译器都提供了一定程度上的代码优化。
然而,大部分由编译器执行的优化仅 涉及执行速度和代码大小 的一个平衡。
你的程序能够变得更快或者更小,但是不可能又变快又变小。
经过本人在嵌入式系统设计和实现过程中实践,下面介绍几种简单且行之有效的C/C++代码的优化方法。
1) Inline函数
在C++中,关键字Inline 可以被加入到任何函数的声明中。
这个关键字 请求编译器用 函数内部的代码替换所有对于指出的函数的调用。
这样做在两个方面快于函数调用。这样做在两个方面快于函数调用:
第一,省去了调用指令需要的执行时间;
第二,省去了传递变元 和 传递过程需要的时间。
但是使用这种方法在优化程序速度的同时,程序长度变大了,因此需要更多的ROM。
使用这种优化在Inline函数频繁调用并且只包含几行代码的时候是最有效的。
2)用指针 代替 数组
在许多种情况下,可以用指针运算 代替数组索引,这样做常常能产生又快又短的代码。
与数组索引相比,指针一般能使代码速度更快,占用空间更少。
使用多维数组时差异更明显。
下面的代码作用是相同的,但是效率不一样。
数组索引 指针运算
for(;;)
{
p=array
A=array[t++];
for(;;)
{
a=*(p++);
...... ......
}
}
指针方法的优点是,array的地址每次装入地址p后,在每次循环中只需对p增量操作。
在数组索引方法中,每次循环中都必须进行基于t值求数组下标的复杂运算。
3)不定义 不使用的返回值
function函数定义 并不知道函数 返回值是否被使用,
假如返回值从来不会被用到,
应该使用void来明确声明函数不返回任何值。
4)手动编写汇编
在嵌入式软件开发中,一些软件模块最好用汇编语言来写,这可以使程序更加有效。
虽然C/C++编译器对代码进行了优化,但是适当的 使用 内联汇编指令 可以有效的提高整个系统运行的效率。
5)使用寄存器变量
在声明 局部变量 的时候可以使用 register关键字。
这就使得编译器把变量放入一个多用途的寄存器中,而不是在堆栈中,
合理使用这种方法可以提高执行速度。
函数调用越是频繁,越是可能提高代码的速度。
6)使用增量和减量操作符
在使用到加一和减一操作时尽量使用增量和减量操作符,因为增量符语句比赋值语句更快,
原因在于对大多数CPU来说,对内存字的增、 减量操作不必明显地使用取内存和写内存的指令,
比如下面这条语句:
x=x+1;
模仿大多数微机汇编语言为例,产生的代码类似于:
move A,x ;把x从内存取出存入累加器A
add A,1 ;累加器A加1
store x ;把新值存回x
如果使用增量操作符,生成的代码如下:
incr x ; x加1
显然,不用取指令和存指令,增、减量操作执行的速度加快,同时长度也缩短了。
7)减少函数调用参数
使用全局变量比函数传递参数更加有效率。
这样做去除了函数调用参数入栈和函数完成后参数出栈所需要的时间。
然而决定使用全局变量会影响程序的模块化和重入,故要慎重使用。
8)Switch语句中 根据 发生频率 来 进行case排序
switch语句是一个普通的编程技术,编译器会产生if-else-if的嵌套代码,
并按照顺序进行比较,发现匹配时,就跳转到满足条件的语句执行。
使用时需要注意。每一个由机器语言实现的测试和跳转仅仅是为了决定下一步要做什么,就把宝贵的处理器时间耗尽。
为了提高速度,没法把具体的情况按照它们发生的相对频率排序。
换句话说,把最可能发生的情况放在第一位,最不可能的情况放在最后。
9)将大的switch语句转为嵌套switch语句
当switch语句中的case标号很多时,为了减少比较的次数,明智的做法是把大switch语句转为嵌套switch语句。
把发生频率高的case 标号放在一个switch语句中,并且是嵌套switch语句的最外层,
发生相对频率相对低的case标号放在另一个switch语句中。
比如,下面的程序段把相对发生频率低的情况放在缺省的case标号内。
pMsg=ReceiveMessage();
switch (pMsg->type)
{
case FREQUENT_MSG1:
handleFrequentMsg();
break;
case FREQUENT_MSG2:
handleFrequentMsg2();
break;
...
case FREQUENT_MSGn:
handleFrequentMsgn();
break;
default: //嵌套部分用来处理不经常发生的消息 ====
switch (pMsg->type)
{
case INFREQUENT_MSG1:
handleInfrequentMsg1();
break;
case INFREQUENT_MSG2:
handleInfrequentMsg2();
break;
......
case INFREQUENT_MSGm:
handleInfrequentMsgm();
break;
}
}
如果switch中每一种情况下都有很多的工作要做,
那么把整个switch语句用一个指向函数指针的表 来替换会更加有效,
比如下面的switch语句,有三种情况:
enum MsgType{Msg1, Msg2, Msg3}
switch (ReceiveMessage()
{
case Msg1;
......
case Msg2;
.....
case Msg3;
.....
}
为了提高执行速度,用下面这段代码来替换这个上面的switch语句。
/*准备工作*/
int handleMsg1(void);
int handleMsg2(void);
int handleMsg3(void);
/*创建一个函数指针数组*/
int (*MsgFunction [])()={handleMsg1, handleMsg2, handleMsg3};//函数指针数组
/*用下面这行更有效的代码来替换switch语句*/
status=MsgFunction[ReceiveMessage()]();
10)避免使用C++的昂贵特性
C++在支持现代软件工程、OOP、结构化等方面对C进行了卓有成效的改进,
但在程序代码容量、执行速度、程序复杂程度等方面比C语言程序性能差一些。
并不是所有的C++特性都是肮贵的。
比如,类的定义是完全有益的。
公有和私有成员数据及函数的列表与一个 struct 及函数原形的列表并没有多大的差别。
单纯的加入类既不会影响代码的大小,也不会影响程序的效率。
但C++的多重继承、虚拟基类、模板、 异常处理及运行类型识别等特性对代码的大小和效率有负面的影响,
因此对于C++的一些特性要慎重使用,可做些实验看看它们对应用程序的影响。
4 总结语
在嵌入式实时程序设计时可以运用上面介绍的一种或多种技术来优化代码。
上面介绍的方法主要是为了提高代码的效率。
但是事实上,在使用这些技术提高代码运行速度的同时会相应的产生一些负面的影响,
比如增加代码的大小、降低程序可读性等。
不过你可以让C/C++编 译器来进行减少代码大小的优化,而手动利用以上技术来减少代码的执行时间。
在嵌入式程序设计中合理地使用这几种技术有时会达到很好 的优化效果。
C/C++代码优化具体方案
目录
1、选择合适的算法和数据结构 3
2、使用尽量小的数据类型 3
3、减少运算的强度 3
(1)查表 3
(2)求余运算 4
(3)平方运算 4
(4)用移位实现乘除法运算 4
(5)避免不必要的整数除法 5
(6)使用增量和减量操作符 5
(7)使用复合赋值表达式 6
(8)提取公共的子表达式 6
4、结构体成员的布局 7
(1)按数据类型的长度排序 7
(2)把结构体填充成最长类型长度的整倍数 7
(3)按数据类型的长度排序本地变量 7
(4)把频繁使用的指针型参数拷贝到本地变量 8
5、循环优化 9
(1)充分分解小的循环 9
(2)提取公共部分 9
(3)延时函数 10
(4)while循环和do…while循环 10
(5)循环展开 10
(6)循环嵌套 11
(7)Switch语句中根据发生频率来进行case排序 12
(8)将大的switch语句转为嵌套switch语句 13
(9)循环转置 14
(10)公用代码块 15
(12)选择好的无限循环 16
6、提高CPU的并行性 16
(1)使用并行代码 16
(2)避免没有必要的读写依赖 17
7、循环不变计算 17
8、函数优化 18
(1)Inline函数 18
(2)不定义不使用的返回值 20
(3)减少函数调用参数 20
(4)所有函数都应该有原型定义 20
(5)尽可能使用常量(const) 21
(6)把本地函数声明为静态的(static) 21
(7)Virtual function的运行期负担 21
9、采用递归及声明放置 22
(1)请使用初始化而不是赋值 22
(2)把声明放在合适的位置上 22
(3)初始化列表 23
10、变量 24
(1)register变量 24
(2)同时声明多个变量优于单独声明变量 25
(3)短变量名优于长变量名,应尽量使变量名短一点 25
(4) 在循环开始前声明变量 25
(5) 把那些保持不变的对象声明为const 25
11、使用嵌套的if结构 25
选择一种合适的数据结构很重要,如果在一堆随机存放的数中使用了大量的插入和删除指令,那使用链表要快得多。
数组与指针语句具有十分密切的关系,一般来说,指针比较灵活简洁,而数组则比较直观,容易理解。
对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。
在许多种情况下,可以用指针运算代替数组索引,这样做常常能产生又快又短的代码。
与数组索引相比,指针一般能使代码速度更快,占用空间更少。
使用多维数组时差异更明显。
能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;
能够使用整型变量定义的变量就不要用长整型(long int),
能不使用浮点型(float)变量就不要使用浮点型变量。
当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,
C编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。
在ICCAVR中,可以在Options中设定使用printf参数,
尽量使用基本型参数(%c、%d、%x、%X、%u和%s格式说明符),
少用长整型参数(%ld、%lu、%lx和%lX格式说明符),
至于浮点型的参数(%f)则尽量不要使用,其他C编译器也一样。
在其他条件不变的情况下,使用%f参数,
会使生成的代码的数量增加很多,执行速度降低。
(1)查表
一个聪明的游戏大虾,基本上不会在自己的主循环里搞什么运算工作,绝对是先计算好了,再到循环里查表。看下面的例子:
旧代码:
long factorial(int i) // 阶乘
{
if (i == 0)
return 1;
else
return i * factorial(i - 1);
}
新代码:
static long factorial_table[] =
{1, 1, 2, 6, 24, 120, 720 // etc };
long factorial(int i)
{
return factorial_table[i];
}
如果表很大,不好写,就写一个init函数,在循环外临时生成表格。
(2)求余运算
a=a%8; // 求2n方的余数, 2^3=8
// 可以改为:
a=a&7; // & (2^n - x)
// 说明:位操作只需一个指令周期即可完成,而大部分的C编译器的”%”运算均是调用子程序来完成,代码长、执行速度慢。
通常,只要求是求2n方的余数,均可使用位操作的方法来代替。
(3)平方运算
a=pow(a, 2.0);
//可以改为:
a=a*a;
/*
说明:在有内置硬件乘法器的单片机中(如51系列),乘法运算比求平方运算快得多,
因为浮点数的求平方是通过调用子程序来实现的,在自带硬件乘法器的AVR单片机中,
如ATMega163中,乘法运算只需2个时钟周期就可以完成。
既使是在没有内置硬件乘法器的AVR单片机中,
乘法运算的子程序比平方运算的子程序代码短,执行速度快。
*/
// 如果是求3次方,如:
a=pow(a,3.0);
// 更改为:
a=a*a*a;
//则效率的改善更明显。
(4)用移位 实现 乘除法 运算
a=a*4;
b=b/4;
// 可以改为:
a=a<<2;
b=b>>2;
/*
通常如果需要乘以或除以2n,都可以用移位的方法代替。
在ICCAVR中,如果乘以2n,都可以生成左移的代码,
而乘以其他的整数或除以任何数,均调用乘除法子程序。
用移位的方法得到代码比调用乘除法子程序生成的代码效率高。
实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如:
*/
a=a*9
//可以改为:
a=(a<<3)+a // a*2^3 + a = 9*a
// 采用运算量更小的表达式替换原来的表达式,下面是一个经典例子:
// 旧代码:
x = w % 8;
y = pow(x, 2.0);
z = y * 33;
for (i = 0;i < MAX;i++)
{
h = 14 * i;
printf(“%d”, h);
}
// 新代码:
x = w&7; // w%8 ---> w&7 位操作比求余运算快
y = x*x; // pow(x, 2.0) ---> x*x 乘法比平方运算快
z = (y << 5) + y; // y*33 ---> y*2^5 +y 位移乘法比乘法快
for (i = h = 0; i < MAX; i++)
{
h += 14; // 14 * i ---> += 14 加法比乘法快 ======!!!!!======
printf(“%d”, h);
}
(5)避免不必要的整数除法
整数除法是整数运算中最慢的,所以应该尽可能避免。
一种可能减少整数除法的地方 是 连除, 这里除法可以由乘法代替。
这个替换的副作用是有可能在算乘积时会溢出,所以只能在一定范围的除法中使用。
// 旧代码:
int i, j, k, m;
m = i / j / k;
// 新代码:
int i, j, k, m;
m = i / (j * k);
(6)使用增量和减量操作符
在使用到加一和减一操作时尽量使用增量和减量操作符,因为增量符语句比赋值语句更快,
原因在于对大多数CPU来说,对内存字的增、减量操作不必明显地使用取存储器和写存储器的指令,
比如下面这条语句:
x=x+1;
模仿大多数微机汇编语言为例,产生的代码类似于:
move A,x ;把x从存储器取出存入累加器A
add A,1 ;累加器A加1
store x ;把新值存回x
如果使用增量操作符源代码如下:
++x;
生成的代码如下:
incr x ;x加1
显然,不用取指令和存指令,增、减量操作执行的速度加快,同时长度也缩短了。
还有,最好用前置,后置需要保存一次。
(7)使用复合赋值表达式
// 复合赋值表达式(如a-=1及a+=1等)都能够生成高质量的程序代码。
// 旧代码:
a=a+b;
// 新代码:
a+=b;
(8)提取公共的子表达式
在某些情况下,C++编译器不能从浮点表达式中提出公共的子表达式,因为这意味着相当于对表达式重新排序。
需要特别指出的是,编译器在提取公共子表达式前不能按照代数的等价关系重新安排表达式。
这时,程序员要手动地提出公共的子表达式(在VC.NET里有一项”全局优化”选项可以完成此工作,但效果就不得而知了)。
旧代码:
float a, b, c, d, e, f;
...
e = b * c / d; // 含 b/d
f = b / d * a; // 也含 b/d
新代码:
float a, b, c, d, e, f;
...
const float t(b / d);
e = c * t;
f = a * t;
旧代码:
float a, b, c, e, f;
...
e = a / c; // 都除以c 也就是包含 1.0f / c
f = b / c;
新代码:
float a, b, c, e, f;
...
const float t(1.0f / c);
e = a * t;
f = b * t;
很多编译器有”使结构体字,双字或四字对齐”的选项。
但是,还是需要改善结构体成员的对齐,有些编译器可能分配给结构体成员空间的顺序与他们声明的不同。
但是,有些编译器并不提供这些功能,或者效果不好。
所以,要在付出最少代价的情况下实现最好的结构体和结构体成员对齐,建议采取下列方法:
(1)按数据类型的长度排序
把结构体的成员按照它们的类型长度排序,声明成员时把长的类型放在短的前面。
编译器要求把长型数据类型存放在偶数地址边界。
在申明一个复杂的数据类型 (既有多字节数据又有单字节数据) 时,
应该首先存放多字节数据,然后再存放单字节数据,这样可以避免存储器的空洞。
编译器自动地把结构的实例对齐在内存的偶数边界。
(2)把结构体填充成最长类型长度的整倍数
把结构体填充成最长类型长度的整倍数。
照这样,如果结构体的第一个成员对齐了,所有整个结构体自然也就对齐了。
下面的例子演示了如何对结构体成员进行重新排序:
旧代码: //普通顺序
struct
{
char a[5];
long k;
double x;
baz;
}
新代码: //新的顺序并手动填充了几个位元组
struct
{
double x; // 长的
long k;
char a[5];
char pad[7];// 并手动填充了几个位元组 5+7=12=3*4 4字节 对齐
baz;
}
这个规则同样适用于类的成员的布局!!!。
(3)按数据类型的长度排序本地变量
当编译器分配给本地变量空间时,它们的顺序和它们在源代码中声明的顺序一样,
和上一条规则一样,应该 把 长的变量 放在 短的变量前面。
如果第一个变量对齐了,其他变量就会连续的存放,而且不用填充字节自然就会对齐。
有些编译器在分配变量时不会自动改变变量顺序,
有些编译器不能产生4字节对齐的栈,所以4字节可能不对齐。
下面这个例子演示了本地变量声明的重新排序:
旧代码,普通顺序
short ga, gu, gi;
long foo, bar;
double x, y, z[3];
char a, b;
float baz;
新代码,改进的顺序
double z[3]; // 长的(老大)放在前面
double x, y;
long foo, bar;
float baz;
short ga, gu, gi;
char a, b;
(4)把频繁使用的指针型参数 拷贝到 本地变量
避免在函数中 频繁使用 指针型参数 指向的值。
因为编译器不知道指针之间是否存在冲突,所以指针型参数往往不能被编译器优化。
这样数据不能被存放在寄存器中,而且明显地占用了存储器带宽。
注意,很多编译器有”假设不冲突”优化开关(在VC里必须手动添加编译器命令行/Oa或/Ow),
这允许编译器假设两个不同的指针总是有不同的内容,这样就不用把指针型参数保存到本地变量。
否则,请在函数一开始把指针指向的数据保存到本地变量。如果需要的话,在函数结束前拷贝回去。
旧代码:
// 假设 q != r
void isqrt(unsigned long a, unsigned long* q, unsigned long* r)
{
*q = a; //
if (a > 0)
{
while (*q > (*r = a / *q))
{
*q = (*q + *r) >> 1;
}
}
r = a - *q * q;
}
新代码:
// 假设 q != r
void isqrt(unsigned long a, unsigned long* q, unsigned long* r)
{
unsigned long qq, rr; // 中间变量,存储对应 两个指针指向的 返回值
qq = a;
if (a > 0)
{
while (qq > (rr = a / qq))
{
qq = (qq + rr) >> 1; // 除以2
}
}
rr = a - qq * qq;
*q = qq; // 最后把 计算结果(中间变量) 赋值给 两个指针指向的返回值
*r = rr;
}
(1)充分分解小的循环
要充分利用CPU的指令缓存(一个指令周期能够读取多个数据),就要充分分解小的循环。
特别是当循环体本身很小的时候,分解循环可以提高性能。
注意:很多编译器并不能自动分解循环。
旧代码: // 3D转化:把矢量 V 和 4x4 矩阵 M 相乘
for (i = 0; i < 4; i ++) // 行
{
r[i] = 0;
for (j = 0; j < 4; j ++) // 列
{
r[i] += M[j][i]*V[j];
}
}
新代码:
r[0] = M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3];
r[1] = M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3];
r[2] = M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3];
r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3];
(2)提取公共部分
对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,
这里的任务包括表达式、函数的调用、指针运算、数组访问等,
应该将没有必要执行多次的操作全部集合在一起,放到一个init的初始化程序中进行。
(3)延时函数
通常使用的延时函数均采用自加的形式:
``c`
void delay (void)
{
unsigned int i;
for (i=0;i<1000;i++) ;
}
将其改为自减延时函数:
```c
void delay (void)
{
unsigned int i;
for (i=1000;i>0;i–) ;
}
两个函数的延时效果相似,但几乎所有的C编译对后一种函数生成的代码均比前一种代码少1~3个位元组,
因为几乎所有的MCU均有,为0转移的指令采用后一种方式能够生成这类指令。
在使用while循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3个字母。
但是在循环中有通过循环变量”i”读写数组的指令时,使用预减循环有可能使数组超界,要引起注意。
(4)while循环和do…while循环
用while循环时有以下两种循环形式:
unsigned int i;
i=0;
while (i<1000)
{
i++;
//用户程序
}
// 或:
unsigned int i;
i=1000;
do
{
i–-;
//用户程序
}
while (i>0);
// 在这两种循环中,使用do…while循环编译后生成的代码的长度短于while循环。
(5)循环展开
这是经典的速度优化,但许多编译程序(如gcc -funroll-loops)能自动完成这个事,
所以现在你自己来优化这个显得效果不明显。
旧代码:
for (i = 0; i < 100; i++)
{
do_stuff(i);
}
新代码:
for (i = 0; i < 100; )
{
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
}
可以看出,新代码里比较指令由100次降低为10次(i每次循环会增加10),循环时间节约了90%。
不过注意:对于中间变量或结果被更改的循环,
编译程序往往拒绝展开,(怕担责任呗),这时候就需要你自己来做展开工作了。
还有一点请注意,在有内部指令cache的CPU上(如MMX芯片),因为循环展开的代码很大,往往cache溢出,
这时展开的代码会频繁地在CPU 的cache和存储器之间调来调去,又因为cache速度很高,所以此时循环展开反而会变慢。
还有就是循环展开会影响矢量运算优化。
(6)循环嵌套
把相关循环放到一个循环里,也会加快速度。
旧代码:
for (i = 0; i < MAX; i++) // initialize 2d array to 0’s
for (j = 0; j < MAX; j++)
a[i][j] = 0.0;
for (i = 0; i < MAX; i++) // put 1’s along the diagonal
a[i][i] = 1.0;
新代码:
for (i = 0; i < MAX; i++) // initialize 2d array to 0’s
{
for (j = 0; j < MAX; j++)
a[i][j] = 0.0;
a[i][i] = 1.0; // put 1’s along the diagonal 对角线1
}
(7)Switch语句中根据发生频率来进行case排序
Switch 可能转化成多种不同算法的代码。其中最常见的是跳转表和比较链/树。
当switch用比较链的方式转化时,编译器会产生if-else-if的嵌套代码,
并按照顺序进行比较,匹配时就跳转到满足条件的语句执行。
所以可以对case的值依照发生的可能性进行排序,把最有可能的放在第一位,这样可以提高性能。
此外,在case中推荐使用小的连续的整数,因为在这种情况下,所有的编译器都可以把switch 转化成跳转表。
旧代码:
int days_in_month, short_months, normal_months, long_months;
.....
switch (days_in_month)
{
case 28:
case 29:
short_months ++; // 短 的 月份
break;
case 30:
normal_months ++; // 正常的月份
break;
case 31:
long_months ++; // 较长的月份
break;
default:
cout << “month has fewer than 28 or more than 31 days” << endl;
break;
}
新代码:
int days_in_month, short_months, normal_months, long_months;
...
switch (days_in_month)
{
case 31:
long_months ++; // 31天的和30天的 出现的较为常见 出现频率较高
break;
case 30:
normal_months ++;
break;
case 28:
case 29:
short_months ++; // 28\29 天的 少见,出现频率低
break;
default:
cout << “month has fewer than 28 or more than 31 days” << endl;
break;
}
(8)将大的switch语句转为嵌套switch语句
当switch语句中的case标号很多时,为了减少比较的次数,明智的做法是把大switch语句转为嵌套switch语句。
把发生频率高的case 标号放在一个switch语句中,并且是嵌套switch语句的最外层,
发生相对频率相对低的case标号放在另一个switch语句中。
比如,下面的程序段把相对发生频率低的情况放在缺省的case标号内。
pMsg=ReceiveMessage();
switch (pMsg->type)
{
case FREQUENT_MSG1:
handleFrequentMsg();
break;
case FREQUENT_MSG2:
handleFrequentMsg2();
break;
...
case FREQUENT_MSGn:
handleFrequentMsgn();
break;
default: //嵌套case部分用来处理不经常发生的消息
switch (pMsg->type)
{
case INFREQUENT_MSG1:
handleInfrequentMsg1();
break;
case INFREQUENT_MSG2:
handleInfrequentMsg2();
break;
......
case INFREQUENT_MSGm:
handleInfrequentMsgm();
break;
}
}
如果switch中每一种情况下都有很多的工作要做,那么把整个switch语句用一个指向函数指针的表来替换会更加有效,比如下面的switch语句,有三种情况:
enum MsgType{Msg1, Msg2, Msg3}
switch (ReceiveMessage() )
{
case Msg1;
...
case Msg2;
...
case Msg3;
...
}
为了提高执行速度,用下面这段代码来替换这个上面的switch语句。
//准备工作
int handleMsg1(void);
int handleMsg2(void);
int handleMsg3(void);
//创建一个函数指针数组
int (*MsgFunction [])()={handleMsg1, handleMsg2, handleMsg3}; // 函数指针数组 返回值为int类型,输入类型无
//用下面这行更有效的代码来替换switch语句
status=MsgFunction[ReceiveMessage()]();
(9)循环转置
有些机器对JNZ(为0转移)有特别的指令处理,速度非常快,如果你的循环对方向不敏感,可以由大向小循环。
旧代码:
for (i = 1; i <= MAX; i++) // 循环变量 小 ---> 大
{
...
}
新代码:
i = MAX+1;
while (–i) // 循环变量 大 ----> 小
{
...
}
不过千万注意,如果指针操作使用了i值,这种方法可能引起指针越界的严重错误(i = MAX+1;)。
当然你可以通过对i做加减运算来纠正,但是这样就起不到加速的作用,除非类似于以下情况:
旧代码:
char a[MAX+5];
for (i = 1; i <= MAX; i++)
{
*(a+i+4)=0;
}
// 新代码:
i = MAX+1;
while (–i)
{
*(a+i+4)=0; // 防止i为父,反向越界
}
(10)公用代码块
一些公用处理模块,为了满足各种不同的调用需要,往往在内部采用了大量的if-then-else结构,
这样很不好,判断语句如果太复杂,会消耗大量的时间的,应该尽量减少公用代码块的使用。
(任何情况下,空间优化和时间优化都是对立的–东楼)。
当然,如果仅仅是一个(3==x)之类的简单判断,适当使用一下,也还是允许的。
记住,优化永远是追求一种平衡,而不是走极端。
(11)提升循环的性能
要提升循环的性能,减少多余的常量计算非常有用(比如,不随循环变化的计算)。
旧代码(在for()中包含不变的if()):
for( i ... )
{
if( CONSTANT0 ) // 循环内部,判断会执行多次
{
DoWork0( i ); // 假设这里不改变CONSTANT0的值
}
else
{
DoWork1( i ); // 假设这里不改变CONSTANT0的值
}
}
新代码:
if( CONSTANT0 ) // 判断只做一次
{
for( i ... )
{
DoWork0( i );
}
}
else
{
for( i ... )
{
DoWork1( i );
}
}
如果已经知道if()的值,这样可以避免重复计算。
虽然旧代码中的分支可以简单地预测,但是由于新代码在进入循环前分支已经确定,就可以减少对分支预测的依赖。
(12)选择好的无限循环 for (; 优于 while (1)
在编程中,我们常常需要用到无限循环,常用的两种方法是while (1) 和 for (;。
这两种方法效果完全一样,但那一种更好呢?然我们看看它们编译后的代码:
// 编译前:
while (1);
// 编译后:
mov eax,1
test eax,eax
je foo+23h
jmp foo+18h
编译前:
for (;;);
编译后:
jmp foo+23h
显然,for (;;)指令少,不占用寄存器,而且没有判断、跳转,比while (1)好。
(1)使用并行代码
尽可能把长的有依赖的代码链分解成几个可以在流水线执行单元中并行执行的没有依赖的代码链。
很多高级语言,包括C++,并不对产生的浮点表达式重新排序,因为那是一个相当复杂的过程。
需要注意的是,重排序的代码和原来的代码在代码上一致并不等价于计算结果一致,因为浮点操作缺乏精确度。
在一些情况下,这些优化可能导致意料之外的结果。
幸运的是,在大部分情况下,最后结果可能只有最不重要的位(即最低位)是错误的。
// 旧代码:
double a[100], sum;int i;
sum = 0.0f;
for (i=0; i<100; i++) // 100次循环
sum += a[i];
// 新代码:
double a[100], sum1, sum2, sum3, sum4, sum;
int i;
sum1 = sum2 = sum3 = sum4 = 0.0; // 100次循环被分解成 25*4次循环
for (i = 0; i < 100; i += 4) // 25次循环
{
sum1 += a[i]; // 1,5,9,...
sum2 += a[i+1]; // 2,6,10,...
sum3 += a[i+2]; // 3,7,11,...
sum4 += a[i+3]; // 4,8,12,...
}
sum = (sum4+sum3)+(sum1+sum2); // 最后对 4段的和再次求和
要注意的是:使用4 路分解是因为这样使用了4段流水线浮点加法,
浮点加法的每一个段占用一个时钟周期,保证了最大的资源利用率。
(2)避免没有必要的读写依赖
当数据保存到内存时存在读写依赖,即数据必须在正确写入后才能再次读取。
虽然AMD Athlon等CPU有加速读写依赖延迟的硬件,允许在要保存的数据被写入内存前读取出来,
但是,如果避免了读写依赖并把数据保存在内部寄存器中,速度会更快。
在一段很长的又互相依赖的代码链中,避免读写依赖显得尤其重要。
如果读写依赖发生在操作数组时,许多编译器不能自动优化代码以避免读写依赖。
所以推荐程序员手动去消除读写依赖,举例来说,引进一个可以保存在寄存器中的临时变量。
这样可以有很大的性能提升。下面一段代码是一个例子:
// 不好的代码:
float x[VECLEN],y[VECLEN], z[VECLEN];
...
for (unsigned int k = 1; k < VECLEN; k++)
{
x[k] = x[k-1] + y[k];
}
for (k = 1; k < VECLEN; k++)
{
x[k] = z[k] * (y[k] - x[k-1]);
}
// 推荐的代码:
float x[VECLEN], y[VECLEN],z[VECLEN];
...
float t(x[0]);
for (unsigned int k = 1; k < VECLEN; k++)
{
t = t + y[k]; // 增加一个中间变量
x[k] = t;
}
t = x[0];
for (k = 1; k <VECLEN; k++)
{
t = z[k] * (y[k] - t);// 增加一个中间变量
x[k] = t;
}
对于一些不需要循环变量参加运算的计算任务可以把它们放到循环外面,
现在许多编译器还是能自己干这件事,不过对于中间使用了变量的算式它们就不敢动了,所以很多情况下你还得自己干。
对于那些在循环中调用的函数,凡是没必要执行多次的操作通通提出来,放到一个init函数里,循环前调用。
另外尽量减少喂食次数,没必要的话尽量不给它传参,需要循环变量的话让它自己建立一个静态循环变量自己累加,速度会快一点。
还有就是结构体访问,东楼的经验,凡是在循环里对一个结构体的两个以上的元素执行了访问,
就有必要建立中间变量了(结构这样,那C++的对象呢? 想想看),看下面的例子:
// 旧代码:
total =
a->b->c[4]->aardvark +
a->b->c[4]->baboon +
a->b->c[4]->cheetah +
a->b->c[4]->dog;
// 新代码:
struct animals * temp = a->b->c[4];// 建立结构体指针变量
total =
temp->aardvark +
temp->baboon +
temp->cheetah +
temp->dog;
// 一些老的C语言编译器不做聚合优化,而符合ANSI规范的新的编译器可以自动完成这个优化,看例子:
float a, b, c, d, f, g;
...
a = b / c * d;
f = b * g / c;
// 这种写法当然要得,但是没有优化
float a, b,c,d,f,g;
...
a = b / c * d;
f = b / c * g;
// 如果这么写的话,一个符合ANSI规范的新的编译器可以只计算b/c一次,
// 然后将结果代入第二个式子,节约了一次除法运算。
(1)Inline函数
在C++中,关键字Inline可以被加入到任何函数的声明中。
这个关键字请求编译器用函数内部的代码替换所有对于指出的函数的调用。
这样做在两个方面快于函数调用:
第一,省去了调用指令需要的执行时间;
第二,省去了传递变元和传递过程需要的时间。
但是使用这种方法在优化程序速度的同时,程序长度变大了,因此需要更多的ROM。
使用这种优化在Inline函数频繁调用并且只包含几行代码的时候是最有效的。
(2)不定义不使用的返回值
函数定义并不知道函数返回值是否被使用,假如返回值从来不会被用到,
应该使用void来明确声明函数不返回任何值。
(3)减少函数调用参数
使用全局变量 比 函数传递参数 更加有效率。
这样做去除了函数调用参数入栈和函数完成后参数出栈所需要的时间。
然而决定 使用全局变量 会影响程序的模块化和重入,故要慎重使用。
(4)所有函数都应该有原型定义
一般来说,所有函数都应该有原型定义。
原型定义可以传达给编译器更多的可能用于优化的信息。
(5)尽可能使用常量(const)
尽可能使用常量(const)。
C++标准规定,如果一个const声明的对象的地址不被获取,允许编译器不对它分配储存空间。
这样可以使代码更有效率,而且可以生成更好的代码。
(6)把本地函数声明为静态的(static)
如果一个函数只在实现它的文件中被使用,把它声明为静态的(static)以强制使用内部连接。
否则,默认的情况下会把函数定义为外部连接。这样可能会影响某些编译器的优化——比如,自动内联。
与LISP之类的语言不同,C语言一开始就病态地喜欢用重复代码循环,
许多C程序员都是除非算法要求,坚决不用递归。
事实上,C编译器们对优化递归调用一点都不反感,相反,它们还很喜欢干这件事。
只有在递归函数需要传递大量参数,可能造成瓶颈的时候,
才应该使用循环代码,其他时候,还是用递归好些。
程序中变量和对象的声明放在什么位置将会对性能产生显著影响。
同样,对postfix和prefix运算符的选择也会影响性能。
这一部分我们集中讨论四个问题:初始化v.s 赋值,
在程序确实要使用的地方放置声明,构造函数的初始化列表,
prefix v.s postfix运算符。
(1)请使用初始化而不是赋值
在C语言中只允许在一个函数体的开头进行变量的声明,然而在C++中声明可以出现在程序的任何位置。
这样做的目的是希望把对象的声明拖延到确实要使用它的时候再进行。
这样做可以有两个好处:
1. 确保了对象在它被使用前不会被程序的其他部分恶意修改。
如果对象在开头就被声明然而却在20行以后才被使用的话,就不能做这样的保证。
2. 使我们有机会通过用初始化取代赋值来达到性能的提升,从前声明只能放在开头,
然而往往开始的时候我们还没有获得我们想要的值,因此初始化所带来的好处就无法被应用。
但是现在我们可以在我们获得了想要的值的时候直接进行初始化,从而省去了一步。
注意,或许对于基本类型来说,初始化和赋值之间可能不会有什么差异,
但是对于用户定义的类型来说,二者就会带来显著的不同,
因为赋值会多进行一次函数调用—-operator =。
因此当我们在赋值和初始化之间进行选择的话,初始化应该是我们的首选。
(2)把声明放在合适的位置上
在一些场合,通过移动声明到合适的位置所带来的性能提升应该引起我们足够的重视。例如:
bool is_C_Needed();
void use()
{
C c1;
if (is_C_Needed() == false)
{
return; //c1 was not needed
}
//use c1 here
return;
}
上面这段代码中对象c1即使在有可能不使用它的情况下也会被创建,
这样我们就会为它付出不必要的花费,有可能你会说一个对象c1能浪费多少时间,
但是如果是这种情况呢:C c1[1000];我想就不是说浪费就浪费了。
但是我们可以通过移动声明c1的位置来改变这种情况:
void use()
{
if (is_C_Needed() == false)
{
return; //c1 was not needed
}
C c1; //moved from the block's beginning
//use c1 here
return;
}
怎么样,程序的性能是不是已经得到很大的改善了呢?
因此请仔细分析你的代码,把声明放在合适的位置上,它所带来的好处是你难以想象的。
(3)初始化列表
我们都知道,初始化列表一般是用来初始化const或者reference数据成员。
但是由于他自身的性质,我们可以通过使用初始化列表来实现性能的提升。
我们先来看一段程序:
class Person
{
private:
C c_1;
C c_2;
public:
Person(const C& c1, const C& c2 ): c_1(c1), c_2(c2)
// :号后面为 初始化列表,传入参数赋值给 内部私有参数 只进行一次初始化
{}
};
// 当然构造函数我们也可以这样写:
Person::Person(const C& c1, const C& c2)
{
c_1 = c1; // 一次初始化 + 赋值
c_2 = c2;
}
究竟二者会带来什么样的性能差异呢,要想搞清楚这个问题,我们首先要搞清楚二者是如何执行的,
先来看初始化列表:数据成员的声明操作都是在构造函数执行之前就完成了,
在构造函数中往往完成的只是赋值操作,然而初始化列表直接是在数据成员声明的时候就进行了初始化,
因此它只执行了一次copy constructor。
再来看在构造函数中赋值的情况:首先,在构造函数执行前会通过default constructor创建数据成员,
然后在构造函数中通过operator =进行赋值。因此它就比初始化列表多进行了一次函数调用。
性能差异就出来了。但是请注意,如果你的数据成员都是基本类型的话,
那么为了程序的可读性就不要使用初始化列表了,因为编译器对两者产生的汇编代码是相同的。
(1)register变量
在声明局部变量的时候可以使用register关键字。
这就使得编译器把变量放入一个多用途的寄存器中,而不是在堆栈中,
合理使用这种方法可以提高执行速度。
函数调用越是频繁,越是可能提高代码的速度。
在最内层循环避免使用全局变量和静态变量,除非你能确定它在循环周期中不会动态变化,
大多数编译器优化变量都只有一个办法,就是将他们置成寄存器变量,而对于动态变量,
它们干脆放弃对整个表达式的优化。尽量避免把一个变量地址传递给另一个函数,虽然这个还很常用。
C语言的编译器们总是先假定每一个函数的变量都是内部变量,
这是由它的机制决定的,在这种情况下,它们的优化完成得最好。
但是,一旦一个变量有可能被别的函数改变,这帮兄弟就再也不敢把变量放到寄存器里了,严重影响速度。
看例子:
a = b();
c(&d);
因为d的地址被c函数使用,有可能被改变,编译器不敢把它长时间的放在寄存器里,
一旦运行到c(&d),编译器就把它放回存储器,如果在循环里,会造成N次频繁的在内存和寄存器之间读写d的动作,
众所周知,CPU在系统总线上的读写速度慢得很。比如你的赛杨300,CPU主频300,总线速度最多66M,为了一个总线读,CPU可能要等4-5个周期.
register specifier被用来告诉编译器一个对象将被会非常多的使用,可以把它放入寄存器中。
例如:
void f()
{
int *p = new int[3000000];
register int *p2 = p; // 地址(指针) 存放在 寄存器变量中
for (register int j = 0; j<3000000; j++)
{
*p2++ = 0;
}
//…use p
delete [] p;
}
循环计数是应用寄存器变量的最好的候选者。当它们没有被存入一个寄存器中,
大部分的循环时间都被用在了从内存中取出变量和给变量赋新值上。
如果把它存入一个寄存器中的话,将会大大减少这种负担。
需要注意的是,register specifier仅仅是对编译器的一个建议。
就好比内联函数一样,编译器可以拒绝把一个对象存储到寄存器中。
另外,现代的编译器都会通过把变量放入寄存器中来优化循环计数。
Register storage specifier并不仅仅局限在基本类型上,它能够被应用于任何类型的对象。
如果对象太大而不能装进寄存器的话,编译器仍然能够把它放入一个高速存储器中,例如cache。
用register storage specifier声明函数型参将会是建议编译器把实参存入寄存器中而不是堆栈中。
例如:
void f(register int j, register Date d);
(2)同时声明多个变量优于单独声明变量
(3)短变量名优于长变量名,应尽量使变量名短一点
(4)在循环开始前声明变量
(5)把那些保持不变的对象声明为const
通过把对象声明为const,编译器就可以利用这个声明把这样一个对象放入寄存器中。
在if结构中如果要判断的并列条件较多,最好将它们拆分成多个if结构,
然后嵌套在一起,这样可以避免无谓的判断。
说明:
该方案主要是考虑到在嵌入式开发中对程序执行速度的要求特别高,所以该方案主要是为了优化程序的执行速度。
注意:
优化是有侧重点的,优化是一门平衡的艺术,它往往要以牺牲程序的可读性或者增加代码长度为代价。
(任何情况下,空间优化和时间优化都是对立的–东楼)。
我做了很多关于程序执行速度方面优化的工(一个示例),我也看过其它人做的优化。
我发现有两个最基本的优化技术总是被人所忽略。
注意,这两个技术并不是避免时机不成熟的优化。
并不是把冒泡排序变成快速排序(算法优化。
也不是语言或是编译器的优化。
也不是把 i*4 写成 i<<2 的优化。
这两个技术是:
1. 使用 一个profiler。
2. 查看程序执行时的汇编码。
使用这两个技术的人将会成功地写出运行快的代码,不会使用这两个技术的人则不行。
使用一个 Profiler
我们知道,程序运行时的90%的时间是用在了10%的代码上。
我发现这并不准确。一次又一次地,我发现,几乎所有的程序会在1%的代码上花了99%的运行时间。
但是,是哪个1%?一个好的 Profiler 可以告诉你这个答案。
就算我们需要使用100个小时在这1%的代码上进行优化,也比使用100个小时在其它99%的代码上优化产生的效益要高得多得多。
问题是什么?人们不用profiler?不是。
我工作过的一个地方使用了一个华丽而奢侈的Profiler,
但是自从购买这个Profiler后,它的包装3年来还是那么的暂新。
为什么人们不用?我真的不知道。有一次,我和我的同事去了一个负载过大的交易所,
我同事坚持说他知道哪里是瓶颈,毕竟,他是一个很有经验的专家。
最终,我把我的Profiler在他的项目上运行了一下,我们发现那个瓶颈完全在一个意想不到的地方。
就像是赛车一样。团队是赢在传感器和日志上,这些东西提供了所有的一切。
你可以调整一下赛车手的裤子以让其在比赛过程中更舒服,但是这不会让你赢得比赛,也不会让你更有竞争力。
如果你不知道你的速度上不去是因为引擎、排气装置、空体动力学、轮胎气压,或是赛车手,那么你将无法获胜。
编程为什么会不同呢?只要没有测量,你就永远无法进步。
这个世界上有太多可以使用的Profiler了。
随便找一个你就可以看到你的函数的调用层次,调用的次数,以前每条代码的时间分解表(甚至可以到汇编级)。
我看过太多的程序员回避使用Profiler,而是把时间花在那些无用的,错误的方向上的“优化”,而被其竞争对手所羞辱。
(译者陈皓注:使用Profiler时,重点需要关注:
1)花时间多的函数以优化其算法,
2)调用次数巨多的函数——如果一个函数每秒被调用300K次,你只需要优化出0.001毫秒,那也是相当大的优化。
这就是作者所谓的1%的代码占用了99%的CPU时间)
C++ Profiler工具
GUN Gropf
Gprof是GNU profiler工具。可以显示程序运行的“flatprofile”,
包括每个函数的调用次数,每个函数消耗的处理器时间。
也可以显示“调用图”,包括函数的调用关系,每个函数调用花费了多少时间。
还可以显示“注释的源代码”,是程序源代码的一个复本,标记有程序中每行代码的执行次数。
C++ Profiler工具
查看汇编代码
几年前,我有一个同事,Mary Bailey,她在华盛顿大学教矫正代数(remedial algebra),
有一次,她在黑板上写下: x + 3 = 5 然后问他的学生“求解x”,然后学生们不知道答案。
于是她写下:__ + 3 = 5 然后,再问学生“填空”,所有的学生都可以回答了。
未知数x 就像是一个有魔法的字母让大家都在想“x意味着代数,而我没有学过代数,所以我就不知道这个怎么做”。
汇编程序就是编程世界的代数。如果某人问我“inline函数是否被编译器展开了?
”或是问我“如果我写下i*4,编译器会把其优化为左移位操作吗?”。
这个时候,我都会建议他们看看编译器的汇编码。这样的回答是不是很粗暴和无用?
通常,在我这样回答了提问者后,提问都通常都会说,对不起,我不知道什么是汇编!
甚至C++的专家都会这么回答。 汇编语言是最简单的编程语言了(就算是和C++相比也是这样的),如:
ADD ESI,x
# 就是(C风格的代码)
ESI += x;
# 而:
CALL foo
# 则是:
foo();
细节因为CPU的种类而不同,但这就是其如何工作的。
有时候,我们甚至都不需要细节,只需要看看汇编码的长啥样,然后和源代码比一比,你就可以知道汇编代码很多很多了。
那么,这又如何帮助代码优化?举个例子,我几年前认识一个程序员认为他应该去发现一个新的更快的算法。
他有一个benchmark来证明这个算法,并且其写了一篇非常漂亮的文章关于他的这个算法。
但是,有人看了一下其原来算法以及新算法的汇编,发现了他的改进版本的算法允许其编译器把两个除法操作变成了一个。
这和算法真的没有什么关系。我们知道除法操作是一个很昂贵的操作,并且在其算法中,
这俩个除法操作还在一个内嵌循环中,所以,他的改进版的算法当然要快一些。
但,只需要在原来的算法上做一点点小的改动——使用一个除法操作,那么其原来的算法将会和新的一样快。
而他的新发现什么也不是。 下一个例子,一个D用户张贴了一个 benchmark 来显示 dmd (Digital Mars D 编译器)在整型算法上的很糟糕,
而ldc (LLVM D 编译器) 就好很多了。对于这样的结果,其相当的有意见。
我迅速地看了一下汇编,发现两个编译器编译出来相当的一致,并没有什么明显的东西要对2:1这么大的不同而负责。
但是我们看到有一个对long型整数的除法,这个除法调用了运行库。而这个库成为消耗时间的杀手,其它所有的加减法都没有速度上的影响
。出乎意料地,benchmark 和算法代码生成一点关系也没有,完全就是long型整数的除法的问题。
这暴露了在dmd的运行库中的long型除法的实现很差。修正后就可以提高速度。
所以,这和编译器没有什么关系,但是如果不看汇编,你将无法发现这一切。
查看汇编代码经常会给你一些意想不到的东西让你知道为什么程序的性能是那样。
一些意想不到的函数调用,预料不到的自傲,以及不应该存在的东西,等等其实所有的一切。
但也不需要成为一个汇编代码的黑客才能干的事。
结论
如果你觉得需要程序有更好的执行速度,
那么,最基本的方法就是
1. 使用一个 profiler 和 愿意去
2. 查看 一下其汇编代码
以找到程序的瓶颈。
只有找到了程序的瓶颈,此时才是真正在思考如何去改进的时候,比如思考一个更好的算法,使用更快的语言优化,等等。
常规的做法是制胜法宝是挑选一个最佳的算法而不是进行微优化。
虽然这种做法是无可异议的,但是有两件事情是学校没有教给你而需要你重点注意的。
第一个也是最重要的,如果你优化的算法没有参与到你程序性能中的算法,
那么你优化他只是在浪费时间和精力,并且还转移了你的注意力让你错过了应该要去优化的部分。
第二点,算法的性能总和处理的数据密切相关的,就算是冒泡排序有那么多的笑柄,但是如果其处理的数据基本是排好序的,
只有其中几个数据是未排序的,那么冒泡排序也是所有排序算法里性能最好的。
所以,担心没有使用好的算法而不去测量,只会浪费时间,无论是你的还是计算机的。
就好像赛车零件的订购速底是不会让你更靠进冠军(就算是你正确安装零件也不会),
没有Profiler,你不会知道问题在哪里,
不去看汇编,你可能知道问题所在,但你往往不知道为什么。