2.2 优化常识
简单的代码修改经常会带来性能的显著提升。下面的章节总结了避免性能缺陷的几个最重要的“优化常识”。这些方法看似微不足道,但许多科学应用程序在应用这些方法后,性能都有了显著提升。
2.2.1 少做工作
重新组织代码以减少代码工作量,在很多情况下可显著提升性能。最常见的例子是循环检测一组对象是否具有特定属性,任一对象具备该属性即可:
如果complex_func()函数没有其他作用,FLAG是唯一和循环外部通信的变量。这种情况下,FLAG值一经改变,就立即退出循环可明显减少计算工作量(取决于条件判断变为true的概率):
2.2.2 避免耗时运算
算法的实现通常采用“步步为营”的策略。首先不做性能方面(因为在进行性能优化时,往往存在更改数值运算的风险)的考虑,将公式直接翻译成代码。第二步使用“便宜”运算替代“昂贵”运算。三角函数和幂运算是“强”运算(“昂贵”运算)的典型代表。记住类似x*2.0的表达式是不会被编译器优化成xx的,而指数(对数)的运算性能是很低的。避免“昂贵”运算的优化方法称为强度消减(strength reduction)。除上述简单情况外,“强”运算有时会关联一组有限的固定参数。下面是一个非平衡自旋系统仿真代码的例子:
程序的最后两行代码包含在一个循环中,并占用该应用程序几乎全部的运行时间。整型变量用于存储自旋取向(向上或者向下,对应值为1或者-1),所以变量edelz的取值范围为{-6,…,+6}。在整个应用程序中,tanh()函数即便用硬件实现也是最耗时的操作(至少几十个时钟周期)。根据以上描述,可根据参数范围将该函数结果转存在一个数组中,循环内部完全消除对tanh()函数的调用。假设tt为定值,那么这个表格只需创建一次:
这个数组存储在访存性能非常高的L1 cache中。因此,相对于tanh()函数,该数组的查找时间可以忽略不计。由于该数组尺寸较小且被频繁调用,所以整个计算过程都会被存储在L1 cache中。
2.2.3 缩减工作集
程序代码在计算过程中或至少在整体运行时间中的内存使用量称为该代码的工作集。一般情况下,压缩工作集会提高cache命中率,对性能提升有正面影响。如何实现工作集压缩以及是否会带来性能提升,很大程度上取决于算法和它的实现。上例中,原始代码使用了4字节的整除存储自旋取向,工作集也因此远远大于所有处理器的L2 cache。如果改变数组定义,使用1字节的整数来存储自旋取向。工作集会因此减小将近4倍,从而接近cache大小。
然而,并不是所有处理器都能有效处理“小”数据类型。如果处理器采用较大的字长,单字节类型数据通过移位和标记操作抽取,那么使用单字节整型数据的程序会非常低效。另一方面,如果可以采用SIMD指令,那么采用简单数据类型的程序就会非常高效(具体见2.3.3节)。