《2022年C++编码优化 .pdf》由会员分享,可在线阅读,更多相关《2022年C++编码优化 .pdf(15页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、C+ 代码优化谈到优化,很多人都会直接想到汇编。难道优化只能在汇编层次吗?当然不是,C+层次一样可以作代码优化,其中有些常常是意想不到的。在 C+层次进行优化, 比在汇编层次优化具有更好的移植性,应该是优化中的首选做法。确定浮点型变量和表达式是 float 型为了让编译器产生更好的代码(比如说产生3DNow! 或 SSE 指令的代码 ), 必须确定浮点型变量和表达式是 float 型的。要特别注意的是,以 ;F; 或 ;f; 为后缀(比如:3.14f)的浮点常量才是 float 型,否则默认是 double 型。为了避免 float 型参数自动转化为 double,请在函数声明时使用 floa
2、t。使用 32 位的数据类型编译器有很多种,但它们都包含的典型的32 位类型是: int,signed,signed int,unsigned,unsigned int,long,signed long,long int ,signed long int ,unsigned long,unsigned long int 。尽量使用32 位的数据类型,因为它们比16 位的数据甚至8 位的数据更有效率。明智使用有符号整型变量在很多情况下, 你需要考虑整型变量是有符号还是无符号类型的。比如, 保存一个人的体重数据时不可能出现负数,所以不需要使用有符号类型。但是,如果是要保存温度数据,就必须使用到有符
3、号的变量。在许多地方,考虑是否使用有符号的变量是必要的。在一些情况下,有符号的运算比较快;但在一些情况下却相反。比如:整型到浮点转化时,使用大于16 位的有符号整型比较快。因为x86 构架中提供了从有符号整型转化到浮点型的指令,但没有提供从无符号整型转化到浮点的指令。看看编译器产生的汇编代码:不好的代码:编译前 编译后名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 1 页,共 15 页 - - - - - - - - - double x; mov foo + 4, 0 unsign
4、ed int i ; mov eax, i x = i ; mov foo, eax flid qword ptr foo fstp qword ptr x 上面的代码比较慢。不仅因为指令数目比较多,而且由于指令不能配对造成的FLID 指令被延迟执行。最好用以下代码代替:推荐的代码:编译前 编译后double x; fild dword ptr i int i; fstp qword ptr x x = i ;在整数运算中计算商和余数时,使用无符号类型比较快。以下这段典型的代码是编译器产生的 32 位整型数除以4 的代码:不好的代码编译前 编译后int i; mov eax, i i = i
5、/ 4 ; cdq and edx, 3 add eax, edx sar eax, 2 mov i, eax 推荐的代码编译前 编译后unsigned int i ; shr i, 2 i = i / 4 ;总结:无符号类型用于:除法和余数循环计数数组下标有符号类型用于:名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 2 页,共 15 页 - - - - - - - - - 整型到浮点的转化while VS. for 在编程中,我们常常需要用到无限循环,常用的两种方法是while
6、(1) 和 for (; ;)。这两种方法效果完全一样,但那一种更好呢?然我们看看它们编译后的代码:编译前 编译后while (1) ; mov eax,1 test eax,eax je foo+23h jmp foo+18h 编译前 编译后for (; ;); jmp foo+23h 一目了然, for (; ;)指令少,不占用寄存器,而且没有判断跳转,比while (1) 好。使用数组型代替指针型使用指针会使编译器很难优化它。因为缺乏有效的指针代码优化的方法,编译器总是假设指针可以访问内存的任意地方,包括分配给其他变量的储存空间。所以为了编译器产生优化得更好的代码, 要避免在不必要的地方
7、使用指针。一个典型的例子是访问存放在数组中的数据。C+ 允许使用操作符 或指针来访问数组,使用数组型代码会让优化器减少产生不安全代码的可能性。比如,x0 和 x2 不可能是同一个内存地址,但 *p 和 *q 可能。强烈建议使用数组型,因为这样可能会有意料之外的性能提升。不好的代码typedef struct float x,y,z,w ; VERTEX ;typedef struct float m44 ;名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 3 页,共 15 页 - -
8、- - - - - - - MATRIX;void XForm(float* res, const float* v, const float* m, int nNumVerts) float dp;int i;const VERTEX* vv = (VERTEX *)v;for (i = 0 ; i ;x * *m + ;dp += vv- ; y * *m + ;dp += vv- ; z * *m + ;dp += vv- ; w * *m + ;*res + = dp ; / 写入转换了的 x dp = vv- ;x * *m + ;dp += vv- ; y * *m + ;dp +
9、= vv- ; z * *m + ;dp += vv- ; w * *m + ;*res + = dp ; / 写入转换了的 y dp = vv- ;x * *m + ;dp += vv- ; y * *m + ;dp += vv- ; z * *m + ;dp += vv- ; w * *m + ;*res + = dp ; / 写入转换了的 z dp = vv- ;x * *m + ;dp += vv- ; y * *m + ;dp += vv- ; z * *m + ;dp += vv- ; w * *m + ;*res + = dp ; / 写入转换了的 w vv + ; / 下一个矢
10、量m -= 16; 推荐的代码typedef struct float x,y,z,w ; VERTEX ;typedef struct float m44 ; MATRIX;void XForm (float* res, const float* v, const float* m, int nNumVerts) 名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 4 页,共 15 页 - - - - - - - - - int i;const VERTEX* vv = (VERTEX
11、*)v;const MATRIX* mm = (MATRIX*)m ;VERTEX* rr = (VERTEX*)res;for (i = 0 ; i ;x = vv- ;x * mm- ;m00 + vv- ;y * mm- ;m01 + vv- ; z * mm- ;m02 + vv- ;w * mm- ;m03 ;rr-;y = vv- ;x * mm- ;m10 + vv- ;y * mm- ;m11 + vv- ; z * mm- ;m12 + vv- ;w * mm- ;m13 ;rr-;z = vv- ;x * mm- ;m20 + vv- ;y * mm- ;m21 + vv-
12、 ; z * mm- ;m22 + vv- ;w * mm- ;m23 ;rr-;w = vv- ; x * mm- ;m30 + vv- ;y * mm- ;m31 + vv- ; z * mm- ;m32 + vv- ;w * mm- ;m33 ; 注意 : 源代码的转化是与编译器的代码发生器相结合的。从源代码层次很难控制产生的机器码。依靠编译器和特殊的源代码,有可能指针型代码编译成的机器码比同等条件下的数组型代码运行速度更快。明智的做法是在源代码转化后检查性能是否真正提高了,再选择使用指针型还是数组型。充分分解小的循环要充分利用CPU 的指令缓存,就要充分分解小的循环。特别是当循环体本身
13、很小的时候,分解循环可以提高性能。BTW: 很多编译器并不能自动分解循环。不好的代码/ 3D 转化:把矢量 V 和 4x4 矩阵 M 相乘for (i = 0 ; i ; 4; i +) ri = 0 ;for (j = 0 ; j ; 4; j +) ri += Mji*Vj; 推荐的代码r0 = M00*V0 + M10*V1 + M20*V2 + M30*V3;r1 = M01*V0 + M11*V1 + M21*V2 + M31*V3;名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - -
14、- 第 5 页,共 15 页 - - - - - - - - - r2 = M02*V0 + M12*V1 + M22*V2 + M32*V3;r3 = M03*V0 + M13*V1 + M23*V2 + M33*v3;避免没有必要的读写依赖当数据保存到内存时存在读写依赖,即数据必须在正确写入后才能再次读取。虽然AMD Athlon等 CPU 有加速读写依赖延迟的硬件,允许在要保存的数据被写入内存前读取出来,但是, 如果避免了读写依赖并把数据保存在内部寄存器中,速度会更快。在一段很长的又互相依赖的代码链中,避免读写依赖显得尤其重要。如果读写依赖发生在操作数组时,许多编译器不能自动优化代码以避
15、免读写依赖。所以推荐程序员手动去消除读写依赖,举例来说,引进一个可以保存在寄存器中的临时变量。这样可以有很大的性能提升。下面一段代码是一个例子:不好的代码float xVECLEN, yVECLEN, zVECLEN;. for (unsigned int k = 1 ; k ; VECLEN ; k +) xk = xk-1 + yk; for (k = 1 ; k ; VECLEN ; k+) xk = zk * (yk - xk-1); 推荐的代码float xVECLEN, yVECLEN, zVECLEN;. float t(x0) ;for (unsigned int k = 1
16、; k ; VECLEN ; k +) t = t + yk ;xk = t ; t = x0 ;for (k = 1 ; k ; VECLEN ; k +) t = zk * (yk - t);xk = t ; 名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 6 页,共 15 页 - - - - - - - - - Switch 的用法Switch 可能转化成多种不同算法的代码。其中最常见的是跳转表和比较链/树。推荐对case的值依照发生的可能性进行排序,把最有可能的放在第一个,当
17、switch 用比较链的方式转化时,这样可以提高性能。此外,在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
18、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 + ;break;case 30: normal_months + ;break;case 28: case 29: short_months +;名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理
19、 - - - - - - - 第 7 页,共 15 页 - - - - - - - - - break;default: cout ; ;month has fewer than 28 or more than 31 days ; ; endl;break; 所有函数都应该有原型定义一般来说, 所有函数都应该有原型定义。原型定义可以传达给编译器更多的可能用于优化的信息。尽可能使用常量 (const) 尽可能使用常量(const)。C+ 标准规定,如果一个const 声明的对象的地址不被获取,允许编译器不对它分配储存空间。这样可以使代码更有效率,而且可以生成更好的代码。提升循环的性能要提升循环的
20、性能,减少多余的常量计算非常有用(比如,不随循环变化的计算)。不好的代码 (在 for() 中包含不变的if() 推荐的代码for( i . ) if( CONSTANT0 ) DoWork0( i ) ; / 假设这里不改变CONSTANT0 的值 else DoWork1( i ) ; / 假设这里不改变CONSTANT0 的值 if( CONSTANT0 ) for( i . ) DoWork0( i ) ;名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 8 页,共 15 页
21、- - - - - - - - - else for( i . ) DoWork1( i ) ; 如果已经知道if() 的值,这样可以避免重复计算。虽然不好的代码中的分支可以简单地预测,但是由于推荐的代码在进入循环前分支已经确定,就可以减少对分支预测的依赖。把本地函数声明为静态的(static) 如果一个函数在实现它的文件外未被使用的话,把它声明为静态的(static) 以强制使用内部连接。否则, 默认的情况下会把函数定义为外部连接。这样可能会影响某些编译器的优化比如,自动内联。考虑动态内存分配动态内存分配(C+中的 ;new ; )可能总是为长的基本类型(四字对齐)返回一个已经对齐的指针。
22、但是如果不能保证对齐,使用以下代码来实现四字对齐。这段代码假设指针可以映射到 long 型。例子double* p = (double*)new BYTEsizeof(double) * number_of_doubles+7L;double* np = (double*)(long(p) + 7L) &; 8L) ;现在,你可以使用 np 代替 p 来访问数据。注意:释放储存空间时仍然应该用delete p。使用显式的并行代码尽可能把长的有依赖的代码链分解成几个可以在流水线执行单元中并行执行的没有依赖的代码链。 因为浮点操作有很长的潜伏期,所以不管它被映射成 x87 或 3DNow! 指令,
23、 这都很重要。 很多高级语言, 包括 C+,并不对产生的浮点表达式重新排序,因为那是一个相当复杂的过程。 需要注意的是, 重排序的代码和原来的代码在代数上一致并不等价于计算结果一致,因为浮点操作缺乏精确度。在一些情况下,这些优化可能导致意料之外的结果。幸运的是,在大部分情况下,最后结果可能只有最不重要的位(即最低位)是错误的。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 9 页,共 15 页 - - - - - - - - - 不好的代码double a100, sum ;int
24、i;sum = 0.0f;for (i=0 ; i;100; i+) sum += ai ;推荐的代码double a100, sum1, sum2, sum3, sum4, sum ;int i;sum1 = sum2 = sum3 = sum4 = 0.0 ;for (i = 0 ; i ; 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) un
25、signed long qq, rr ;qq = a;if (a ; 0) while (qq ; (rr = a / qq) qq = (qq + rr) ; 1; rr = a - qq * qq ;*q = qq ;*r = rr ; 名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 13 页,共 15 页 - - - - - - - - - 赋值与初始化先看看以下代码:class CInt int m_i ;public: CInt(int a = 0):m_i(a) cout
26、 ; ;CInt ; ; endl; CInt() cout ; ; CInt ; ; endl; CInt operator + (const CInt& ; a) return CInt(m_i + a.GetInt(); void SetInt(const int i) m_i = i; int GetInt() const return m_i ; ;不好的代码void main() CInt a, b, c;a.SetInt(1);b.SetInt(2);c = a + b; 推荐的代码void main() CInt a(1), b(2) ;CInt c(a + b) ; 这两段代
27、码所作的事都一样,但那一个更好呢?看看输出结果就会发现,不好的代码输出了四个 ;CInt ;和四个 ;CInt ; ,而推荐的代码只输出三个。也就是说,第二个例子比第一个例子少生成一次临时对象。Why? 请注意,第一个中的c 用的是先声明再赋值的方法,第二个用的是初始化的方法,它们有本质的区别。第一个例子的;c = a + b;先生成一个临时对象用来保存a + b 的值,再把该临时对象用位拷贝的方法给c 赋值,然后临时对象被销毁。这个临时对象就是那个多出来的对象。第二个例子直接用拷贝构造函数的方法对c初始化,不产生临时对象。所以,尽量在需要使用一个对象时才声明,并用初始化的方法赋初值。名师资料
28、总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 14 页,共 15 页 - - - - - - - - - 尽量使用成员初始化列表在初始化类的成员时,尽量使用成员初始化列表而不是传统的赋值方式。不好的代码class CMyClass string strName;public: CMyClass(const string& ; str);CMyClass:CMyClass(const string&; str) strName = str; 推荐的代码class CMyClass strin
29、g strName;int i;public: CMyClass(const string& ; str);CMyClass:CMyClass(const string&;str) : strName(str) 不好的例子用的是赋值的方式。这样,strName 会先被建立 (调用了string 的默认构造函数) ,再由参数str 赋值。而推荐的例子用的是成员初始化列表,strName 直接构造为str,少调用一次默认构造函数,还少了一些安全隐患。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 15 页,共 15 页 - - - - - - - - -