AVX版pyrDown结果错误且性能劣于SSE2,求问题排查与优化
问题描述
我分别用SSE2和AVX指令集实现了两个pyrDown图像降采样函数,但AVX版本不仅输出错误的图像结果(错误结果见下图),运行速度还比SSE2版本更慢,不符合预期。请帮忙排查AVX实现中的问题,并给出性能优化方案。

相关实现代码
SSE2/SSSE3 版本
// SSE2 implementation static __inline __m128i average2RowsSingle(const uint8_t* __restrict__ src, size_t srcStep) { __m128i v0 = _mm_load_si128((const __m128i *)src); __m128i v1 = _mm_load_si128((const __m128i *)&src[srcStep]); return _mm_avg_epu8(v0, v1); } // SSSE3 version // I used `__restrict__` to give the compiler more flexibility in unrolling void average2Rows(const uint8_t* __restrict__ src, uint8_t*__restrict__ dst, size_t srcStep, size_t size) { const __m128i vk1 = _mm_set1_epi8(1); const __m128i add2 = _mm_set1_epi16(2); size_t dstsize = size/2; for (size_t i = 0; i < dstsize - 15; i += 16) { const size_t ii = i*2; // based on https://stackoverflow.com/a/45564565/820795 __m128i left = average2RowsSingle(src+ii, srcStep); __m128i right = average2RowsSingle(src+ii+16, srcStep); __m128i w0 = _mm_maddubs_epi16(left, vk1); // unpack and horizontal add __m128i w1 = _mm_maddubs_epi16(right, vk1); w0 = _mm_srli_epi16(w0, 1); // divide by 2 w1 = _mm_srli_epi16(w1, 1); w0 = _mm_packus_epi16(w0, w1); // pack _mm_storeu_si128((__m128i *)&dst[i], w0); } }
AVX 版本
// AVX implementation static __m256i average2RowsSingle(const uint8_t* __restrict__ src, size_t srcStep) { auto v0 = _mm256_load_si256((const __m256i*)src); auto v1 = _mm256_load_si256((const __m256i*)&src[srcStep]); return _mm256_avg_epu8(v0, v1); } void average2Rows(const uint8_t* __restrict__ src, uint8_t*__restrict__ dst, size_t srcStep, size_t size) { const __m128i vk1 = _mm_set1_epi8(1); size_t dstsize = size/2; const signed char o = -1; // make shuffle zero const __m256i vec_r_i16 = _mm256_set_epi8(o,30, o,28, o,26, o,24, o,22, o,20, o,18, o,16, o,14, o,12, o,10, o, 8, o, 6, o, 4, o, 2, o, 0); const __m256i vec_l_i16 = _mm256_set_epi8(o,31, o,29, o,27, o,25, o,23, o,21, o,19, o,17, o,15, o,13, o,11, o, 9, o, 7, o, 5, o, 3, o, 1); for (size_t i = 0; i < dstsize - 31; i += 32) { const size_t ii = i * 2; auto left = average2RowsSingle(src + ii, srcStep); auto right = average2RowsSingle(src + ii + 32, srcStep); auto w0 = _mm256_shuffle_epi8(left, vec_r_i16); auto w1 = _mm256_shuffle_epi8(left, vec_l_i16); left = _mm256_srli_epi16(_mm256_add_epi16(w0, w1), 1); w0 = _mm256_shuffle_epi8(right, vec_r_i16); w1 = _mm256_shuffle_epi8(right, vec_l_i16); right = _mm256_srli_epi16(_mm256_add_epi16(w0, w1), 1); left = _mm256_packus_epi16(left, right); _mm256_storeu_si256((__m256i *) &dst[i], left); } }
问题排查(AVX版本错误&性能差原因)
1. 核心计算逻辑错误
SSE版本通过_mm_maddubs_epi16实现相邻两个uint8像素的打包求和(将每对相邻8位无符号数转成16位并相加),但AVX版本的shuffle掩码设计完全错误:
vec_r_i16和vec_l_i16的掩码是从高位到低位取偶数/奇数索引的像素,相加后得到的是间隔一个像素的求和,而非相邻像素,直接导致降采样后的像素值计算错误,出现图像失真。- 该逻辑完全偏离了pyrDown需要的"相邻水平像素平均"的需求。
2. 内存访问未对齐
AVX的_mm256_load_si256要求32字节对齐,若src指针未对齐,会触发非对齐访问,不仅性能暴跌,还可能在部分平台引发运行错误。这是AVX版本比SSE慢的核心原因之一。
3. 循环边界未处理
AVX版本的循环仅处理了dstsize - 31的部分,剩余31个像素未做处理,会导致图像边缘缺失或错误。
4. 不必要的类型转换
AVX版本中vk1定义为__m128i,但实际需要__m256i,隐式转换会带来额外开销。
性能优化&修复方案
1. 修复核心计算逻辑
方案A:使用AVX2指令(推荐,若平台支持)
直接用_mm256_maddubs_epi16对齐SSE版本的逻辑,简化代码同时保证正确性:
// 修复后的AVX2版本核心循环 void average2Rows(const uint8_t* __restrict__ src, uint8_t*__restrict__ dst, size_t srcStep, size_t size) { const __m256i vk1 = _mm256_set1_epi8(1); size_t dstsize = size/2; // 处理批量像素 for (size_t i = 0; i < dstsize - 31; i += 32) { const size_t ii = i * 2; __m256i left = average2RowsSingle(src + ii, srcStep); __m256i right = average2RowsSingle(src + ii + 32, srcStep); // 和SSE逻辑一致:相邻像素求和→除以2→打包 __m256i w0 = _mm256_maddubs_epi16(left, vk1); __m256i w1 = _mm256_maddubs_epi16(right, vk1); w0 = _mm256_srli_epi16(w0, 1); w1 = _mm256_srli_epi16(w1, 1); __m256i packed = _mm256_packus_epi16(w0, w1); _mm256_storeu_si256((__m256i*)&dst[i], packed); } // 处理剩余像素 for (size_t i = dstsize - (dstsize % 32); i < dstsize; ++i) { const size_t ii = i * 2; uint8_t p0 = (src[ii] + src[ii + srcStep]) / 2; uint8_t p1 = (src[ii+1] + src[ii+1 + srcStep]) / 2; dst[i] = (p0 + p1) / 2; } }
方案B:仅支持AVX(无AVX2)
用拆包指令模拟相邻像素求和:
// AVX无AVX2时的替代逻辑 __m256i even = _mm256_unpacklo_epi8(left, _mm256_setzero_si256()); __m256i odd = _mm256_unpackhi_epi8(left, _mm256_setzero_si256()); odd = _mm256_srli_epi16(odd, 8); // 移位对齐奇数位 __m256i sum = _mm256_add_epi16(even, odd); sum = _mm256_srli_epi16(sum, 1); // 除以2
2. 优化内存访问
- 尽可能保证
src和dst指针为32字节对齐,使用_mm256_load_si256/_mm256_store_si256替代非对齐指令,提升内存带宽利用率。 - 若无法保证对齐,强制使用
_mm256_loadu_si256/_mm256_storeu_si256,避免未对齐访问的性能损失。
3. 编译与循环优化
- 启用编译器高级优化:
-O3 -mavx2(支持AVX2时)或-O3 -mavx,让编译器自动完成循环展开、指令调度。 - 手动循环展开(若编译器未自动处理),减少循环分支开销。
4. 修正辅助变量类型
将AVX版本中的vk1改为__m256i类型:const __m256i vk1 = _mm256_set1_epi8(1);,消除隐式转换开销。
内容的提问来源于stack exchange,提问作者Gralex
相关产品推荐
相关产品推荐

