基于AVX优化卷积函数未提速的原因及优化方案咨询
我尝试用AVX指令集优化卷积函数,之前用_m256i做类似优化时实现了10倍性能提升,但这次优化后函数耗时几乎没变化。
原函数代码
void setConv(int idx,double* kernel,int kWidth) { double kernel0 = kernel[0]; if (!r || !g || !b) return; double sumrX = r[idx] * kernel0; double sumrY = r[idx] * kernel0; double sumgX = g[idx] * kernel0; double sumgY = g[idx] * kernel0; double sumbX = b[idx] * kernel0; double sumbY = b[idx] * kernel0; for (int ri = 1; ri < kWidth; ri++) { double kernelri = kernel[ri]; int ri_nc = ri*_nc; sumrY += kernelri * (r[idx - ri_nc] + r[idx + ri_nc]); sumrX += kernelri * (r[idx - ri] + r[idx + ri]); sumgY += kernelri * (g[idx - ri_nc] + g[idx + ri_nc]); sumgX += kernelri * (g[idx - ri] + g[idx + ri]); sumbY += kernelri * (b[idx - ri_nc] + b[idx + ri_nc]); sumbX += kernelri * (b[idx - ri] + b[idx + ri]); } yrConv[idx] = sumrY; xrConv[idx] = sumrX; ygConv[idx] = sumgY; xgConv[idx] = sumgX; ybConv[idx] = sumbY; xbConv[idx] = sumbX; }
AVX优化版本代码
(__m256d* sumY与__m256d* sumX已在其他位置分配内存,用于替代原xxConv数组)
void setConv(int idx, double* kernel, int kWidth) { if (!r || !g || !b) return; __m256d sumrgb = _mm256_setr_pd(r[idx], g[idx], b[idx], 0); __m256d kernel0 = _mm256_set1_pd(kernel[0]); sumX[idx] = _mm256_mul_pd(sumrgb, kernel0); sumY[idx] = sumX[idx]; for (int ri = 1; ri < kWidth; ri++) { __m256d kernelri = _mm256_set1_pd(kernel[ri]); int ri_nc = ri * _nc; __m256d sumrgb1 = _mm256_setr_pd(r[idx - ri], g[idx - ri], b[idx - ri], 0); __m256d sumrgb2 = _mm256_setr_pd(r[idx + ri], g[idx + ri], b[idx + ri], 0); __m256d sumrgbX = _mm256_add_pd(sumrgb1, sumrgb2); sumX[idx] = _mm256_fmadd_pd(kernelri, sumrgbX, sumX[idx]); sumrgb1 = _mm256_setr_pd(r[idx - ri_nc], g[idx - ri_nc], b[idx - ri_nc], 0); sumrgb2 = _mm256_setr_pd(r[idx + ri_nc], g[idx + ri_nc], b[idx + ri_nc], 0); __m256d sumrgbY = _mm256_add_pd(sumrgb1, sumrgb2); sumY[idx] = _mm256_fmadd_pd(kernelri, sumrgbY, sumY[idx]); } }
请问我在优化过程中是否存在错误?有无更优的实现方式?
现有优化的核心问题
内存访问模式差,带宽瓶颈严重
优化版依然是按单像素索引零散访问r/g/b数组,每次加载单个double值后用_mm256_setr_pd拼成向量。这种随机零散的内存访问完全没利用AVX的向量加载优势,反而因频繁的标量转向量操作增加了额外开销,CPU缓存对连续内存访问的效率远高于随机访问,相当于把向量指令当标量用,自然看不到性能提升。向量利用率低
AVX2的__m256d可容纳4个double值,但你只用到前3个(r/g/b),第4个位置填0,每次计算都浪费了1/4的向量运算能力,没把硬件性能用满。循环内冗余操作
循环里每次都调用_mm256_set1_pd(kernel[ri]),重复做标量转向量的操作,完全可以提前把整个kernel转换成向量数组,避免循环内的冗余计算。不必要的内存往返
每次循环都把sumX[idx]和sumY[idx]写回内存,下一次循环又读回来,频繁的内存读写会拖慢速度,应该把中间结果放在寄存器里,循环结束后再一次性写回。
更优的实现思路
重构内存布局,按连续向量访问
不要把r/g/b分成三个独立数组,改成按像素打包存储(比如用struct { double r, g, b; }数组),这样可以用_mm256_loadu_pd(未对齐内存)或_mm256_load_pd(对齐内存)一次性加载多个像素的r/g/b数据。如果没法改内存布局,至少批量加载连续索引的r/g/b值,减少零散访问。满额利用向量宽度
可以一次处理2个像素的r/g/b(6个double值),用两个__m256d向量承载;或者调整计算逻辑,把第4个向量位置利用起来(比如同时处理相邻像素的某个通道,或合并X/Y方向的计算)。预加载kernel向量
提前把kernel数组转换成__m256d数组,每个kernel元素都扩展成4个相同值的向量,循环内直接取用预生成的向量,避免重复调用_mm256_set1_pd。减少循环内的内存读写
把sumX和sumY的当前值放在寄存器里,循环结束后再写回内存,示例修改:__m256d currentX = sumX[idx]; __m256d currentY = sumY[idx]; for (int ri = 1; ri < kWidth; ri++) { // ... 计算逻辑 ... currentX = _mm256_fmadd_pd(kernelri, sumrgbX, currentX); currentY = _mm256_fmadd_pd(kernelri, sumrgbY, currentY); } sumX[idx] = currentX; sumY[idx] = currentY;对齐内存
确保sumX、sumY及r/g/b数组的内存是32字节对齐的(AVX2的向量对齐要求),用_mm_malloc或编译器对齐属性(如GCC的__attribute__((aligned(32))))分配内存,这样可以用_mm256_load_pd/_mm256_store_pd替代未对齐指令,提升访问效率。
额外注意点
- 必须开启编译器优化选项:GCC/Clang用
-O3 -mavx2,MSVC用/O2 /arch:AVX2,否则编译器不会生成高效的AVX代码。 - 优化版把
if (!r || !g || !b)判断移到了kernel[0]访问之前,修复了原代码的空指针隐患,这点是正确的。
内容的提问来源于stack exchange,提问作者Gerald

