指针别名
终于来到号称优化杀手的指针别名了!
先来看一个小例子:
可以看到c被重复赋值了两次,那为什么不优化掉呢?
因为可能存在
void func(int *a, int *b , int *c) {
*c = *a;
*c = *b;
}
int main(){
int a , b;
func(&a, &b, &b);//在这里首先是将a->b,b->b;也就是最终b里存的是a
}
如果你想让编译器放心优化,不要担心指针别名,可以这样写:
可以看到加入了__restrict关键字之后,gcc就会开启优化。就不会重复两遍了。
__restrict只需要在非const里就可以了,因为const的指针都是只读的,必然就不会被写入,所以就不用担心别名的问题。
对比的是volatile,这个是让编译器不要优化
可以看到编译器不会自动优化了
而volatile可以针对更多的类型,而__restrict只针对指针。
合并写入 —矢量化指令
两个int32可以被合并写入为一个int64
四个int32可以被合并为一个_m128
可以看到直接来到了128位的xmm寄存器。
而SIMD的加速填充:
可以重点看一下画圆圈的部分,可以看到一次减了16,这是因为每次都存入四个数,一个数是4个字节。伪代码可以视为:
而如果不确定是不是4的倍数:
void func(int *a , int n) {
for(int i =0;i<n;i++){
a[i] = i;
}
}
那么编译器会进行特化,比如要是1023,就会对前1020的数据进行SIMD写入,然后对剩下的三个以标准的方式写入。
循环
如上图所示,可见编译版本很复杂,似乎生成了一个一次16个字节的SIMD版本和一个一次4个字节的普通版本。这是由于编译器害怕指针别名的现象出现,也就是它不清楚会不会修改a里的值,b也会跟着改变,所以会做一个判断,看a+1024和b的大小比较的结果来决定使用哪一个版本。
也可以使用openmp语句将其优化。
可以看到这两个的汇编语言中,是没有任何区别的,这就是编译器自动将不会变的量移到了循环的外边,也就是我们常说的,要将热的代码移到冷的里面去。
尽可能顺序访问也是优化的关键
循环展开:
相当于这样,但不太建议手动展开,会影响SIMD矢量化。
结构体
可以看到,只是加了一个无关的z,编译器就不会采用SIMD并行了,这是因为在只有x,y的时候,是一次输入16个字节,两个结构体。而如果输入了一个z之后,一个结构体就变成了12个字节,所以无法达到并行的要求,所以编译器就自动变成了最基本的低效模式。
使用关键字 alignas 也可以达到同样的效果:
结构体的内存布局:
AOS和SOA
一直没弄懂这两个结构体布局,我们今天来好好看一下:
AOS(Array of Struct)单个对象的属性紧挨着存:xyzxyzxyzxyz
SOA(Struct of Array)属性分离存储在多个数组:xxxxyyyyzzzz
AOS 必须对齐到 2 的幂才高效,SOA 就不需要。
AOS 符合直觉,不一定要存储在数组这种线性结构,而 SOA 可能无法保证多个数组大小一致
SOA 不符合直觉,但通常是更高效的!
这就是AOS,是一个不能优化的代码;
这是SOA,优化后的结果。
但可以看到,这种SOA是不利于面向对象编程的,更多的是面向数据编程。
所以推出了一种中间方案:
AOSOA:
这样也可以起到很好的优化效果。
STL:
stl中的vector也存在诸如此类的问题,可以通过pragma unroll 或者pradma omp simd等解决优化问题。
MATH
正常来说,编译器是可以将a/2优化为a*0.5的,这是因为乘法要比除法运算快得多。
而在下面这个例子中,可以看到并没有进行优化,这是因为编译器不敢保证b是不是为0
优化方案1:手动优化:
1024*乘法时间+1*除法时间<1024*除法时间
优化方案2 :指令调优:
-ffast-math 选项让 GCC 更大胆地尝试浮点运算的优化,如果可以保证不会出现NAN和无穷大,那么就可以使用这个指令。