c++程序性能的初步优化与分析
来源:互联网 发布:it程序员学徒干什么的 编辑:程序博客网 时间:2024/04/27 21:24
#include <math.h>#include <stdio.h>#include <stdlib.h>#define PI 3.14159265359float sx, sy;float sdCircle(float px, float py, float r) { float dx = px - sx, dy = py - sy; return sqrtf(dx * dx + dy * dy) - r;}float opUnion(float d1, float d2) { return d1 < d2 ? d1 : d2;}#define T px + scale * r * cosf(theta), py + scale * r * sin(theta)float f(float px, float py, float theta, float scale, int n) { float d = 0.0f; for (float r = 0.0f; r < 0.8f; r += 0.02f) d = opUnion(d, sdCircle(T, 0.05f * scale * (0.95f - r))); if (n > 0) for (int t = -1; t <= 1; t += 2) { float tt = theta + t * 1.8f; float ss = scale * 0.9f; for (float r = 0.2f; r < 0.8f; r += 0.1f) { d = opUnion(d, f(T, tt, ss * 0.5f, n - 1)); ss *= 0.8f; } } return d;}int ribbon() { float x = (fmodf(sy, 0.1f) / 0.1f - 0.5f) * 0.5f; return sx >= x - 0.05f && sx <= x + 0.05f;}int main(int argc, char* argv[]) { int n = argc > 1 ? atoi(argv[1]) : 3; float zoom = argc > 2 ? atof(argv[2]) : 1.0f; for (sy = 0.8f; sy > 0.0f; sy -= 0.02f / zoom, putchar('\n')) for (sx = -0.35f; sx < 0.35f; sx += 0.01f / zoom) { if (f(0, 0, PI * 0.5f, 1.0f, n) < 0.0f) { if (sy < 0.1f) putchar('.'); else { if (ribbon()) putchar('='); else putchar("............................#j&o"[rand() % 32]); } } else putchar(' '); }}
将代码保存为 milo_tree.c (如上),拿到电脑上跑一遍,感觉圣诞树的生成速度很慢,在 Linux 下编译,用 time 指令看看具体时间:
$ gcc milo_tree.c -o milotree -lm$ time ./milotree . . . ==. . o.. . . ......#.. . ..... ....o.... ..... ..= == ==j .. . . &.. . . . .. ....... j# . ..&... # .&. & ..#... . # . . .....==== = = = # ======....# . .& ..... .. . . . ## ...&... #. . . .. ...... . ..... . .....= == . j o . & .#...==== = = . . . = ===.& . = .. . ....&.# . .. . . . .... .....#... ...j . . . . #o...... ..... ......j= = = .#o. . ..........========= . .&.. . . .#=== =====.... ....& . . = ..j ....... ... . = . . #...................... . o . . . . .&.#.....................==== = = & ......... ... .. ..&.=== == === .......&. #.. . . ..=== = ====... . ..&.& . . ... . .= = == =.. . .......#. . ... .. . &. . = ... ................... .o. . . . &#. ....................#.#..#.====== === . . . . . ..#.....o... .. . o...=== = == ............ . . . .. ......... .. j.. === =====.... .o. ... .. ......... ..... . . . ==== == = .... ....&.... .... . .. .... . . . ... = = . &.. ......o.... ... . . . .. ......... .. ......... ......... ......... ........../milotree 18.34s user 0.00s system 99% cpu 18.384 total
18384 毫秒,很慢的速度。扫一眼代码,里面的嵌套 for 和大量的浮点数运算可能是生成速度慢的原因。但对于新手来说,靠猜测去判断程序的瓶颈在哪里是很不靠谱的。
这里就需要使用专门的性能分析工具,Linux 平台可使用 gprof
,Windows 平台的 Visual Studio 也有类似的功能。
这里我在 Linux 平台下演示,使用 grpof
需要在编译时加上 -pg
参数,然后运行一遍,才可以分析出来。如下指令:
$ gcc milo_tree.c -lm -pg -o milotree$ ./milotree && gprof ./milotreeFlat profile:Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 54.98 1.74 1.74 2911 0.60 1.03 f 26.53 2.58 0.84 224976635 0.00 0.00 sdCircle 13.51 3.00 0.43 230460959 0.00 0.00 opUnion 5.40 3.17 0.17 frame_dummy 0.00 3.17 0.00 702 0.00 0.00 ribbon...
可以看到,代码内的 f 函数是性能瓶颈,sdCircle()
,opUnion()
函数的结构很简单,但由于调用次数接近 2 亿,也存在一定开销。
性能分析结束,开启 GCC 标准优化选项 O2 编译一遍:
$ gcc -O2 milo_tree.c -o milotree -lm && time ./milotree..../milotree 1.85s user 0.00s system 99% cpu 1.860 total
瞬间缩短到 1860 毫秒,9 倍左右的速度提升,这时 gprof
分析一下,结果是:
Flat profile:Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls Ts/call Ts/call name 100.57 1.68 1.68 f Call graph (explanation follows)granularity: each sample hit covers 2 byte(s) for 0.60% of 1.68 secondsindex % time self children called name 5484324 f [1][1] 100.0 1.68 0.00 0+5484324 f [1] 5484324 f [1]-----------------------------------------------Index by function name [1] f
可以看到,即使没有对 sdCircle()
和 opUnion()
加 inline
关键字,它们也被编译器自动展开了。
一般到这里,“优化” 就可以结束了。可一切都是编译器替我们做的,而且可能还期待执行速度可以更快一些。
这时,稍有一点经验的程序员可以从程序的具体实现入手。
接下来,从简单的函数开始,来分析优化点,看 sdCircle()
函数的代码:
float sdCircle(float px, float py, float r) { float dx = px - sx, dy = py - sy; return sqrtf(dx * dx + dy * dy) - r;}
这里使用了 C 标准库的平方根算法的单精度浮点版本 sqrtf()
。这里提一下,标准库的实现都是稳健的,但不是所有都是高效率的。
在特定硬件平台,有专门的指令集提供这类数学上的运算指令,例如可以使用 C 的内联汇编来实现一个平方根运算:
// GCC 版本内联汇编,使用 SSE 指令集float asm_sse_sqrtf(float x){ float ret; // GCC inline assembly __asm__ __volatile__ ( "rsqrtss %1, %%xmm0\n\t" "rcpss %%xmm0, %%xmm0\n\t" "movss %%xmm0, %0" : "=m"(ret) : "m"(x)); return ret;}
但是这个程序并不需要精确到小数点后几十位的程度,精度只要足够即可,否则只是浪费处理器的时钟周期。
这时,回忆起雷神之锤 3 经典的快速平方根倒数算法,里面利用了一个魔数0x5f3759df
求平方根倒数的近似算法。当然,平方根也可以用魔数来快速运算,这篇文章提供了十几种快速平方根算法,其中就有几个是利用了魔数运算。
这里,我把上篇文章中的第 7 个算法简化,实现一个快速平方根运算函数,因为是为了优化,所以我定义成 inline
函数:
typedef unsigned int u32;inline float f_sqrt(float x){ u32 i = (*(u32 *) &x + 0x3f800000) >> 1; return *(float*) &i;}
因为至少需要 32 位的无符号整型才能满足运算的数值范围,由于现代编译器在 32 位和 64 位操作系统上,int 型都是 32 位,所以用 int 的无符号版本即可。
另外,此算法可以通过牛顿迭代法来提高计算的精度,不过在此程序里没什么必要。
这时,再看一看代码,里面有个很慢的随机数算法 rand()
,同样它是 C 标准库提供的。要优化这个算法,可以浏览这篇 Intel 的快速随机数算法文章 ,里面介绍的 LCG 算法比标准库实现快 5 倍左右。
为了一些便利,我用 C++ 替代 C 来写 LCG 算法,并用 C 标准库 time()
的返回值作为种子值,确保每次运行时圣诞树的装饰都不一样:
...#include <ctime>typedef unsigned int u32;class LCG {public: LCG(u32 seed) : mSeed(seed) {} u32 operator()() { mSeed = mSeed * 214013U + 2531011U; return (mSeed >> 16U) & 0x7FFFU; }private: u32 mSeed;};...int main(int argc, char *argv[]){... LCG rng(time(0));...}
编译、运行:
$ g++ i.cc -O2 && time ./a.out./a.out 1.67s user 0.00s system 99% cpu 1.682 total
可以看到比原来的代码快了 200 毫秒左右。
这时候,再对 f 函数下手,浏览一下此函数的代码。第一个 for 循环进行 40 次迭代,虽然看似很简单,但是把宏
#define T px + scale * r * cos(theta), py + scale * r * sin(theta)
- 1
- 1
替换进去,可以看到里面包含了平方根,正弦,余弦的运算,实际整个函数的瓶颈就在此处,应该重点优化。但由于这个 for 循环是浮点数运算,且每次迭代都会用到上一次的结果,想要使用OpenMP 做并行很难,且效果差。
但稍微理解下 f()
函数的用意后,可以利用 opUnion()
函数做模拟并行,具体代码如下:
#define T px + scale * r * f_cos(theta), py + scale * r * f_sin(theta)#define T2 px + scale * (r + 0.02f) * cos(theta), py + scale * \ (r + 0.02f) * sin(theta)float f(float px, float py, float theta, float scale, int n){ float d = 0.0f, di = 0.0f; float ret; for (float r = 0.0f; r < 0.8f; r += 0.04f) { d = opUnion(d, sdCircle(T, 0.05f * scale * (0.95f - r))); di = opUnion(di, sdCircle(T2, 0.05f * scale * (0.93f - r))); } ret = opUnion(d, di); if (n > 0) for (int t = -1; t <= 1; t += 2) { float tt = theta + t * 1.8f; float ss = scale * 0.9f; for (float r = 0.2f; r < 0.8f; r += 0.1f) { ret = opUnion(ret, f(T, tt, ss * 0.5f, n - 1)); ss *= 0.8f; } } return ret;}
接下来对 sin()
和 cos()
动手,虽然不知道有没有魔数的方法来求近似值,但学过高数的同学会知道这两个函数都可以用泰勒级数来求近似值。由于上面提到精度足够即可,在具体实现中我只迭代了 10 次。具体实现如下:
// sin(x) = x - (x^3 / 3!) + (x^5 / 5!) - (x^7 / 7!) + ...template <typename T>T f_sin(const T& x){ const T x2 = x * x; T power = x; T facter = 1; T sign = 1; T sum = 0; const int loop = 22; // 10 times loop for (int i = 3; i < loop; i += 2) { sign *= -1; power *= x2; facter *= i * (i - 1); sum += sign * power / facter; } return sum + x;}// cos(x) = 1 - (x^2 / 2!) + (x^4 / 4!) - (x^6 / 6!) + ...template <typename T>T f_cos(const T& x){ const T x2 = x * x; T power = 1; T facter = 1; T sign = 1; T sum = 0; const int loop = 21; // 10 times loop for (int i = 2; i < loop; i += 2) { sign *= -1; power *= x2; facter *= i * (i - 1); sum += sign * power / facter; } return sum + 1;}
因为这个两个函数实现可用于整型运算,所以我写成模板的形式。可能有更优雅的实现方式,而我只是按照公式写出实现。但这没什么,一切都可以交付给强大的编译器,去优化我的代码。
这时,如果用的是 GCC 编译器,可以
- 开启
-ffast-math
选项来加速浮点数运算; - 开启
-march=native
来让编译器做本地处理器架构优化; - 开启最高等级优化选项
-O3
,O3 和 fast-math 可以合写成-Ofast
:
$ g++ -Ofast -march=native i.cc && time ./a.out$ gcc -Ofast -march=native milo_tree.c -o milotree -lm && time ./milotree./a.out 0.35s user 0.00s system 99% cpu 0.350 total./milotree 0.95s user 0.01s system 96% cpu 0.990 total
Milo Yip 的版本是 88 毫秒,优化后的是 35 毫秒,可能会觉得没有太大提升,这时把圣诞树大小放大 10 倍,来直观感受一下两者执行速度的差距:
$ g++ -Ofast -march=native i.cc && time ./a.out 3 10$ gcc -Ofast -march=native milo_tree.c -o milotree -lm && time ./milotree 3 10./a.out 3 10 34.01s user 0.01s system 99% cpu 34.139 total./milotree 3 10 90.25s user 0.01s system 99% cpu 1:30.32 total
Milo Yip 的版本是 1 分 30 秒 32
优化后的版本是 34 秒 139
因此可以说一个好的算法,或者说一个适合的算法,在程序中发挥着重要作用。因此我们可以得出以下结论:
- 如果程序对性能没有极高的需求,就直接用编译器来为我们做优化;
- 在运算量较大的场合,可以适当取近似值来优化运算的开销;
- 为具体需求选择合适的代码实现
当然,编译器优化并非百利而无一害,比如对运算精度需求极高的程序,千万不可开启 -ffast-math
;
在较底层的代码中,会因为过度依赖编译器的优化选项,而造成现有代码无法直接迁移到编译器的更新版本上(比如 Linux 内核的某些底层实现、硬件驱动等,就过度依赖 GCC 的某一版本),去年我在 Linux 3.4 内核中也发现过类似问题
对性能做到良好取舍,写出合适的代码,都不是一日之功,希望日后可以有更多机会继续深入地学习相关方面的知识。
注:
- 优化后的完整代码
原文:http://blog.csdn.net/andytimes/article/details/51619469
- c++程序性能的初步优化与分析
- 程序性能的初步优化与分析(以 C++ 为例)
- c程序性能优化
- C 程序性能优化
- 程序性能与优化
- 与性能优化有关的几个程序
- linux服务器的性能分析与优化
- linux服务器的性能分析与优化
- linux服务器的性能分析与优化
- 性能分析与优化的故事
- linux服务器的性能分析与优化
- oprofile性能分析优化程序
- c程序性能优化读书笔记
- 程序中GDI资源占用与泄漏的初步分析
- 程序性能的优化
- VTune 分析和优化程序性能的工具
- tomcat性能分析与优化
- JVM性能分析与优化
- 第十五章 Android性能优化
- Java环境变量设置解析
- ZZY的八皇后
- 使用Springboot快速搭建项目
- 《Android 开发艺术探索》读书笔记
- c++程序性能的初步优化与分析
- Cgroup文件系统
- 神经网络深入(连载6)物种形成
- [Leetcode] 68. Text Justification 解题报告
- javascript 布尔对象
- 跬步系列 - word2vector
- Unity3D研究院之使用Animation编辑器编辑动画
- SpringMVC
- 过年前提起这种事就心酸!