列序与行序寻址模式下代码vectorization的疑问及优化咨询
1. 列序寻址时到底什么被向量化了?
你看,编译器提示的是第21行的循环(也就是for (k = 0; k < 3; k++) { VRC[k] = p_M[k][s] - p_M[k][t]; })被向量化了,但因为列序布局下,p_M[k][s]是从不同的列数组里取相同索引的元素,属于非连续的内存访问,编译器只能用AVX2的gather指令(也就是你看到的__builtin_ia32_gatherdiv2df)来实现向量化——这种指令会一次性从分散的内存地址加载数据到向量寄存器里。
至于那些missed: statement clobbers memory的提示,其实是编译器的一种“标注”:gather指令本身会访问内存,编译器认为这个操作涉及内存读写,所以标记为missed,但本质上这个循环确实已经被向量化了,只是用了针对分散访问的特殊向量指令。
而行序布局的情况,p_M[s][k]是同一行里的连续元素,内存访问是连续的,编译器可以用普通的向量加载指令完成向量化,不需要gather,所以也就没有那行关于gather的missed提示。
2. 行序方式的向量化结果有差异吗?
从编译器的输出看,两者都成功向量化了那个k循环,但实际的向量化效率和底层指令差异很大:
- 行序用的是连续内存的向量加载,这是CPU向量单元最擅长的操作,执行效率很高;
- 列序用的是gather指令,这种指令处理分散内存访问的开销比连续访问大很多,虽然也算向量化,但性能不如行序。
不过最终的计算结果是完全一致的,只是性能上有差距。
3. 有没有办法把所有计算(包括VRR累加和sqrt)都向量化?
当然可以,核心是让编译器(或者手动用intrinsics)把整个计算流程都纳入向量操作的范围:
自动向量化方案:
- 把数据布局改成行序(这是基础,连续内存是高效向量化的前提);
- 把数组padding到4个元素(对应AVX2的256位向量宽度,刚好装4个double);
- 可以把循环和平方累加的逻辑稍微调整,让编译器能识别出可向量化的模式,比如:
// 假设p_M每行有4个元素,第4个是0 double vec_diff[4]; for (k = 0; k < 4; k++) { vec_diff[k] = p_M[s][k] - p_M[t][k]; } // 编译器可以自动向量化这个平方和计算 VRR = vec_diff[0]*vec_diff[0] + vec_diff[1]*vec_diff[1] + vec_diff[2]*vec_diff[2] + vec_diff[3]*vec_diff[3]; VR = sqrt(VRR);
编译器会把减法、平方都用向量指令完成,然后用水平累加指令计算VRR,最后再做sqrt(sqrt本身也有向量版本,但因为VRR是标量,这里最后一步是标量sqrt,不过前面的核心计算都向量化了)。
手动intrinsics方案(更直接):
如果想完全手动控制向量操作,可以用AVX2的内置函数:
#include <immintrin.h> // 行序布局,每行4个double,第4个为0 __m256d vec_s = _mm256_load_pd(p_M[s]); __m256d vec_t = _mm256_load_pd(p_M[t]); __m256d vec_diff = _mm256_sub_pd(vec_s, vec_t); __m256d vec_sq = _mm256_mul_pd(vec_diff, vec_diff); // 水平累加向量里的4个元素得到VRR __m128d low_half = _mm256_castpd256_pd128(vec_sq); __m128d high_half = _mm256_extractf128_pd(vec_sq, 1); low_half = _mm_add_pd(low_half, high_half); double temp[2]; _mm_storeu_pd(temp, low_half); double VRR = temp[0] + temp[1]; double VR = sqrt(VRR);
这样整个减法、平方、累加的过程都用向量指令完成,效率最高。
4. 额外的零填充(padding)会有帮助吗?
肯定会,原因有两个:
- 匹配向量宽度:AVX2的向量是256位,刚好能装4个double。如果只处理3个元素,编译器只能用部分向量宽度,浪费了向量单元的算力;padding到4个元素后,可以用完整的向量指令一次性处理所有数据,向量化效率更高。
- 不影响计算结果:因为你填充的是0,平方后还是0,加到VRR里不会改变最终的结果。
另外,配合行序布局的话,padding后的内存访问是完全连续的,编译器可以生成最优化的向量指令,完全避免gather这种低效操作;就算是列序布局,padding到4列也能让gather指令一次性加载4个元素,比处理3个元素的情况要好一些。
备注:内容来源于stack exchange,提问作者Stanislav Fyodorov

