You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AVX版pyrDown结果错误且性能劣于SSE2,求问题排查与优化

问题描述

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

错误的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 09:20:33