说明:程序员
无心看到一篇小短文,猜想做者应该是一个图形学领域的程序员或专家,介绍了在光线(射线)追踪程序中是如何优化C/C++代码的。倒也有一些参考意义,固然有的地方我并不赞同或者说我也不彻底理解,原文在此,个人粗糙翻译以下:算法
1. 牢记Ahmdal定律编程

- funccost表示是函数func的运行时间百分比,funcspeedup是你优化后函数的运行系数;
- 因此,若是函数TriangleIntersect()占用40%的运行时间,而在你优化后使它运行快了两倍,那么你的程序运行可以快了25%;
- 这意味着不常用的代码不须要作过多优化(或者彻底不优化),好比场景加载过程;
- 也就是:让频繁调用的代码运行得更加高效,而让较少调用的代码保持运行正确;
2. 先有正确的代码,而后再作优化数组
- 这并非说先花8个周时间写一个全功能的光线追踪器,而后再花8个周去优化;
- 而是在你的管线追踪程序中的多个阶段都进行优化;
- 若是代码是正确的,而你又知道哪些函数会被频繁的调用,优化是很明显的;
- 而后找到瓶颈所在,并去除瓶颈(经过优化或者算法改进)。一般来讲改进算法能够很显著地优化瓶颈——甚至可能采用了一个你没想到的算法。优化那些你所知道的将被频繁调用的函数是一个很好的作法;
3. 那些我认识的可以写出很是高效的代码的人说,他们花费在优化代码上的时间是他们写代码时间的至少两倍以上 缓存
4. 跳转/分支语句是昂贵的,无论什么时候尽量的减小使用数据结构
- 函数调用除了栈存储操做外,还须要两次跳转;
- 优先选择迭代,而不是递归;
- 若是是短函数,使用内联来消除函数开销;
- 将循环放在函数内(例如将for(i=0;i<100;i++) DoSomething();改成在DoSomething()内作DoSomething());
- 长长的if...else if...else if...else if...语句链须要大量的跳转才能结束(除了在测试每一个条件时)。若是可能,改成switch语句,有时编译器能够有优化为在一个表中查找和单级跳转。若是switch语句是不可能的,那把最常常走到的if语句放在语句链开头;
5. 考虑数组索引的顺序多线程
- 两维或更多维的数组在内存中还是按一维存储的。这意思是array[i][j]和 array[i][j+1]是相邻的(C/C++代码),然而array[i][j]和array[i+1][j]却能够相离的任意远;
- 访问物理内存中的连续数据,能够显著加快你的代码(有时是一个数量级,甚至更多);
- 如今CPU从主内存中加载数据到高速缓存时,它不只仅是只加载单一数据,而是加载一块数据,既包含了要请求的数据,也包含部分相邻数据(一个cache行)。这意思是说若是array[i][j]在CPU缓存中,那么array[i][j+1]就颇有可能也在缓存中了,然而array[i+1][j]可能仍在内存中;
6. 考虑指令级并行性(IPL)ide
- 尽管不少程序还是单线程执行,但现代的CPU已经可以在单核上有显著的并行性。这意味着单CPU也可能同时执行4个浮点数乘法、等待4个内存请求,并执行即将到来的分支比较操做
- 为了充分利用这种并行性,代码块(好比在跳转语句中)须要足够的独立指令来使CPU获得充分使用;
- 能够考虑经过展开循环来改进;
- 这也是使用内联函数的一个很好的缘由;
7. 避免或减小局部变量的使用函数
- 局部变量一般是存储在栈上。若是不多,能够存储在寄存器中。在这种状况下,函数不只获得了对存储在寄存器上的数据的更快内存访问的好处,也能够避免创建一个栈帧的开销;
- 可是,也不要把全部对象都全盘声明为全局变量;
8. 减小函数参数的个数oop
- 和减小局部变量的缘由同样——他们也是在栈上存储的;
9. 结构体(包括类)传参时使用传引用而不是传值
- 在光线追踪程序中,哪怕是简单如vector、points、colors等结构,我也没有见过使用值传递的代码
10. 若是你不须要一个函数的返回值,那就不要返回
11. 尽量避免使用转型操做
- 整数和浮点数的指令集一般在不一样的寄存器上运算,所以转型操做须要拷贝操做;
- 短整形(char和short)仍然须要一个全尺寸的寄存器,并且在存储回内存以前,它们须要对齐到32位或64位上,而后才转换成更小尺寸类型;
12. 当定义C++对象时必定要当心
- 使用初始化(Color c(black))而不是赋值(Color c, c = black),而前者更快;
13. 使类的默认构造函数尽量的轻量
- 特别是那简单的、常用的类(例如,颜色,矢量,点等);
- 这些默认构造函数一般是在你不注意时就调用,甚至那时你并不但愿这样;
- 使用构造初始化列表(使用Color::Color() : r(0), g(0), b(0) {}而不是Color::Color() { r = g = b = 0; } );
14. 尽量使用移位操做符>>和<<,而不是整数乘法和除法
15. 当心使用查表功能
- 不少人鼓励对于复杂的功能(例如,三角函数)使用预先计算过值的查表法。对于光线跟踪程序来讲,这每每是没必要要的。内存查找是很是(日益)昂贵的,并且从新计算三角函数每每和从内存中查找值同样快(尤为是当你考虑到内存查找会影响CPU缓存命中率时);
- 在其它状况下,查表多是很是有用的。好比在GPU编程中,查表法一般是复杂功能的优先选择;
16. 对于大多数的类类型,使用运算符 +=,-=,*=和/=,而少用+,-,*,/
- 这类简单操做其实须要建立一个匿名名的、临时的中间对象;
- 例如Vector v = Vector(1,0,0) + Vector(0,1,0) + Vector(0,0,1) 语句建立了5个未命名、临时的Vector:Vector(1,0,0), Vector(0,1,0),Vector(0,0,1),Vector(1,0,0) + Vector(0,1,0),以及Vector(1,0,0) + Vector(0,1,0) + Vector(0,0,1);
- 稍微更好点的作法:Vector v(1,0,0); v+= Vector(0,1,0); v+= Vector(0,0,1); 这样仅仅建立了2个临时Vector:Vector v(1,0,0) 和Vector(0,0,1),而节省了6个函数调用(3个构造和3个析构);
17. 对于基本数据类型,使用运算符+,-,*,/,而少用+=,-=,*=和/=
18. 延迟局部变量的定义时间
- 定义一个对象总会有一个函数调用开销(就是构造函数)
- 若是一个对象只是有时候才被使用(好比在一个if语句内部),那么就只在必要时才定义,由于这样就只当这个变量使用时才会调用它的构造函数
19. 对于对象来讲,使用前缀操做符(++obj),而不是后缀操做符(obj++)
- 在你的光线追踪程序中,这可能并非个问题
- 对象的拷贝操做必须使用后缀操做符(这须要额外调用一个构造和一个析构函数),而前缀操做符并不产生临时对象
20. 慎用模板
- 各类具现化实例的优化方式多是不一样的;
- 标准模板库(STL)作了很好的优化,但若是你打算实现交互式光线跟踪器,最好是仍避免使用;
- 经过本身实现,你能清楚地明白要它使用的算法,你就会知道最有效的使用方式;
- 更重要的是,个人经验代表调试、编译STL会很慢。一般这也是没问题的,除非你使用Debug版本进行性能分析。你会发现STL的构造、迭代器等操做会占用运行时间的15%以上,它会使输出的分析结果更为混乱
21. 在计算过程当中避免动态内存分配
- 动态内存主要优点在于存储场景数据和其余数据,而不是在计算过程当中进行修改
- 然而,在许多(大多数)时候系统动态存储分配要求使用锁来控制访问分配器。对于使用动态内存的多线程应用程序来讲,因为须要等待分配和释放内存,经过这些额外的处理,你可能实际上获得的是一个更慢的程序
- 即便在单线程程序中,在堆上分配内存也比在栈上分配更昂贵。操做系统须要进行一些计算来肯定所需大小的内存块。
22. 发现和充分利用有关你的系统内存Cache的有用信息
- ü 若是一个数据结构大小刚好填满一个Cache行,处理整个类只须要从内存中读取一次;
- ü 确保全部的数据结构都能对齐到Cache边界(若是你的数据大小和Cache都是128字节,那么当1个字节在一个Cache行而另外127字节在第二个Cache行时,那么性能仍然很差)
23. 避免没必要要的数据初始化
- 若是你要初始化一大块内存,考虑用memset()函数
24. 尽可能提前结束循环判断和函数返回
- 考虑射线和三角形相交。常见状况是射线和三角形不相交,所以这里能够优化;
- 若是你要判断射线和三角形相交的状况,一旦t值射线平面为负,你能够当即返回。这样可使你跳过大约一半的光线三角形交叉点的重心坐标计算。一个巨大的胜利!一旦你肯定没有相交发生,求交函数就应该退出
- 一样的,一些循环也能够被提前结束。例如,在光线阴影设置中,最近的相交是没必要要的。只要发现了任何交叉闭环,求交函数就能够返回
25. 先在纸上简化你使用的公式
- 在不少公式中,老是能够或者一些特殊状况下,能够取消计算
- 编译器找不到这些简化,可是你能够。消除一些内在循环中的昂贵操做能够比你在其余地方的优化更能加速你的程序
26. 对于整数型、定点数、32位浮点数、64位浮点数来讲,他们之间的差异并无你想象中的那么大
- 现代CPU进行浮点运算和整数运算其实有相同的运算吞吐量,像光线追踪这种计算密集型的程序,这意思是整数和浮点运算成本之间的差别能够忽略不计,这意味着你不须要作一些优化来使用整数运算;
- 双精度浮点运算并不必定比单精度浮点计算更慢,尤为是在64位机器上。我曾经在同一台机器上测试光线追踪算法,结果是有时所有使用double比所有使用float会运行得更快,
27. 考虑经过重写你的数学公式来消除昂贵的操做
- sqrt()函数一般是能够避免的,尤为是在比较数值的平方是否相等时;
- 若是你须要反复除以x,考虑计算1/x,而后相乘。在向量的归一化操做中(3次除法),这曾经是一个很大的优化,但最近我发现这很难说。然而若是你整除更屡次数,这样作还是有益的;
- 若是你执行一个循环操做,将那些在循环中固定不变的计算移出到循环外;
- 考虑是否可以在计算循环自增中获得值(而不是每次迭代都计算),原文:Consider if you can compute values in a loop incrementally (instead of computing from scratch each iteration).
- 上句改成:考虑是否可在循环中增量的计算结果,而不是在每次迭代时都从新计算(好比斐波那契数列)。该句由评论中的网友@clover_toeic所翻译。