自研列优先2D数组归约性能异常:消去行耗时为消去列的两倍
性能差异根本原因
你观察到的耗时差异和二维数组的遍历无关,核心差异来自输出数组的写入模式和编译器优化空间的不同:
你的二维数组是列优先存储,两种归约模式对x的读取都是严格的顺序内存访问,完全命中CPU缓存,已经达到了内存带宽上限。区别在于对输出数组y的操作:
- 消去列维度时:内层循环每次写入
y[j],j随内层循环顺序递增,所有写入操作无数据依赖,编译器可以自动生成AVX/SIMD向量化指令,单次循环可以同时完成4~8个double的累加,充分利用CPU算力。 - 消去行维度时:内层循环70000次操作都在累加同一个
y[i]地址,存在强数据依赖,编译器无法做向量化优化,只能执行串行的标量加法,同时CPU的指令级并行(ILP)能力也无法发挥,算力直接降到原来的1/2甚至更低。
你的测试数据也符合这个判断:消去列的2.2秒对应内存读取带宽约18GB/s,刚好是你设备DDR4内存的正常读取速度,瓶颈在内存;消去行的4.5秒瓶颈已经转移到CPU的标量计算能力。
慢路径优化方案
方案1:临时变量累加优化(适用于求和类归约)
如果你的归约操作是累加这类可以用临时变量中转的场景,直接修改归约逻辑,把累加操作放在寄存器级的临时变量中,内层循环结束后再写回内存,就能大幅提升性能:
case drop::rows: if(this->d1!=ii.size(2)){string msg="err";throw msg;} for(i64 i=0;i<ii.size(2);++i){ T sum = T{}; for(i64 j=0;j<ii.size(1);++j){ sum += ii(j,i); } (*this)(i) = sum; } break;
修改后消去行的耗时会和消去列基本持平。
方案2:分块优化(适用于任意通用lambda)
如果你需要支持任意自定义lambda,不能修改归约操作的调用逻辑,可以采用分块访问的方式:将列维度分成32/64列的小块,每次处理一个小块内的所有数据,既保持x的顺序读取,又能让编译器对小块内的多个y元素操作做向量化优化,性能可以提升60%以上。
方案3:多线程并行优化
如果对性能要求更高,可以在外层循环加上OpenMP并行指令,利用多核CPU的算力,代码改动最小:
#pragma omp parallel for for(i64 i=0;i<ii.size(2);++i){ // 原有内层循环逻辑 }
编译时加上-fopenmp参数即可,8核CPU下性能可以再提升5~7倍。
内容的提问来源于stack exchange,提问作者user1407220
相关产品推荐
相关产品推荐

