You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何利用AVX2优化v210转p210格式的性能?当前AVX2实现性能下降30%的优化建议及慢操作分析

如何利用AVX2优化v210转p210格式的性能?当前AVX2实现性能下降30%的优化建议及慢操作分析

兄弟,我太懂你现在的糟心了——本来想靠AVX2给4K 60Hz的v210转p210提速,结果手写的AVX2代码反而比标量实现慢了30%,这换谁都得挠头。我来帮你拆解下当前代码里拖慢性能的关键环节,再给点实打实的优化思路,咱们把性能拉回来甚至超过标量实现。

先揪出当前实现里的慢操作

你的代码思路没问题,但细节上的冗余操作和指令选择拖了后腿:

  • 过度依赖shuffle和blend指令:你用了3次_mm256_shuffle_epi8和4次_mm256_blend_epi16,这些指令虽然灵活,但AVX2的vpshufb(对应shuffle_epi8)每个周期只能执行1次,多次调用会形成性能瓶颈,延迟也不低。
  • 冗余的零填充与pack操作_mm256_packs_epi32(s0_32, zeroes)把32位值转16位时填充了大量零,后面又要靠shuffle去掉这些零,完全是做无用功,平白增加了指令数和延迟。
  • 拆分存储的额外开销:每次处理完都要把256位寄存器拆成两个128位存储,_mm256_extracti128_si256加两次_mm_storeu_si128,这一系列操作增加了内存访问的往返次数,拖慢了整体节奏。
  • 非对齐内存访问:你用的_mm256_loadu_si256是非对齐加载,在大循环里,非对齐内存访问的延迟会被持续放大,尤其是4K分辨率下的高帧率场景。
  • 循环内的分支判断:代码里lineNo == height - 1的分支会导致分支预测失效,高帧率下分支预测失败的开销会不断累积,影响整体性能。

针对性优化建议,直接落地见效

1. 重构数据提取逻辑,砍掉冗余的shuffle/blend

v210的打包规则是4个32位字存12个10bit分量(每个32位字里是[2bit填充 + 10bit分量 + 10bit分量 + 10bit分量]),咱们可以直接用AVX2的位操作一次性把分量提取到正确位置,不用先拆分再 shuffle。

比如把提取和移位合并:原来你先提取10bit到32位寄存器,再pack成16位,最后左移6位转成16bit有效值。现在可以直接在提取时就左移6位,一步到位得到16bit的目标值,省去后续的pack和移位指令:

// 直接提取低10位并左移6位,得到16bit有效值
__m256i s0 = _mm256_slli_epi32(_mm256_and_si256(dwords, mask10), 6);
// 提取中间10位(右移10位后取低10位),再左移6位
__m256i s1 = _mm256_slli_epi32(_mm256_and_si256(_mm256_srli_epi32(dwords, 10), mask10), 6);
// 提取高10位(右移20位后取低10位),再左移6位
__m256i s2 = _mm256_slli_epi32(_mm256_and_si256(_mm256_srli_epi32(dwords, 20), mask10), 6);

2. 用高效的排列指令替代shuffle+blend

AVX2的_mm256_permutevar8x32_epi32指令可以按自定义掩码直接重排32位单元,吞吐量比vpshufb高很多。咱们可以预定义掩码,一次性把Y分量和UV分量分别提取到连续的寄存器位置,替代多次shuffle和blend操作:

// 预定义排列掩码:把所有Y分量的位置挑出来
const __m256i y_perm_mask = _mm256_setr_epi32(0, 2, 4, 6, 8, 10, -1, -1);
// 预定义排列掩码:把所有UV分量的位置挑出来
const __m256i uv_perm_mask = _mm256_setr_epi32(1, 3, 5, 7, 9, 11, -1, -1);

// 把三个寄存器的分量合并成连续的16bit值
__m256i all_components = _mm256_packus_epi32(_mm256_packus_epi32(s0, s1), s2);
// 一次性提取Y和UV分量
__m256i y_vals = _mm256_permutevar8x32_epi32(all_components, y_perm_mask);
__m256i uv_vals = _mm256_permutevar8x32_epi32(all_components, uv_perm_mask);

3. 优化内存访问,尽量对齐

如果你的输入源数据可以对齐到32字节(比如用_mm_malloc分配内存,或者给数组加上__declspec(align(32))属性),把_mm256_loadu_si256换成_mm256_load_si256,对齐加载的延迟比非对齐低很多,大循环里效果明显。

输出的Y和UV平面也尽量对齐到16字节以上,减少存储操作的延迟。

4. 拆分循环,去掉分支判断

把最后一行的处理单独拎出来,主循环只处理前height-1行,这样主循环里没有分支,分支预测不会失效,性能会提升不少:

// 处理前height-1行,无分支
for (int lineNo = 0; lineNo < height - 1; ++lineNo) {
    // 完整处理每一行的所有组
    ...
}
// 单独处理最后一行
if (height > 0) {
    int lineNo = height - 1;
    // 处理最后一行的所有组,按需处理边界
    ...
}

5. 拉满编译器优化选项

MSVC下一定要开启/O2(或/Ox)优化,同时加上/arch:AVX2选项,让编译器帮你做指令重排、冗余指令消除等优化,最大化AVX2指令的效率。

优化后代码示例(简化版)

void convert(const uint8_t* src, int srcStride, uint8_t* dstY, uint8_t* dstUV, int width, int height) {
    const int groupsPerLine = width / 12;
    const __m256i mask10 = _mm256_set1_epi32(0x3FF);
    // 预定义Y和UV分量的排列掩码
    const __m256i y_perm_mask = _mm256_setr_epi32(0, 2, 4, 6, 8, 10, -1, -1);
    const __m256i uv_perm_mask = _mm256_setr_epi32(1, 3, 5, 7, 9, 11, -1, -1);

    // 处理前height-1行,无分支
    for (int lineNo = 0; lineNo < height - 1; ++lineNo) {
        const uint32_t* srcLine = reinterpret_cast<const uint32_t*>(src + lineNo * srcStride);
        uint16_t* dstLineY = reinterpret_cast<uint16_t*>(dstY + lineNo * width * 2);
        uint16_t* dstLineUV = reinterpret_cast<uint16_t*>(dstUV + lineNo * width * 2);

        for (int g = 0; g < groupsPerLine; ++g) {
            // 加载8个32位字(对应2组12个分量,共24个分量)
            __m256i dwords = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(srcLine));
            
            // 提取所有10bit分量并直接转成16bit有效值
            __m256i s0 = _mm256_slli_epi32(_mm256_and_si256(dwords, mask10), 6);
            __m256i s1 = _mm256_slli_epi32(_mm256_and_si256(_mm256_srli_epi32(dwords, 10), mask10), 6);
            __m256i s2 = _mm256_slli_epi32(_mm256_and_si256(_mm256_srli_epi32(dwords, 20), mask10), 6);
            
            // 合并成连续的16bit分量数组
            __m256i all_components = _mm256_packus_epi32(_mm256_packus_epi32(s0, s1), s2);
            
            // 提取Y和UV分量
            __m256i y_vals = _mm256_permutevar8x32_epi32(all_components, y_perm_mask);
            __m256i uv_vals = _mm256_permutevar8x32_epi32(all_components, uv_perm_mask);
            
            // 存储Y分量
            _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineY), _mm256_castsi256_si128(y_vals));
            _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineY + 6), _mm256_extracti128_si256(y_vals, 1));
            
            // 存储UV分量
            _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineUV), _mm256_castsi256_si128(uv_vals));
            _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineUV + 6), _mm256_extracti128_si256(uv_vals, 1));
            
            dstLineY += 12;
            dstLineUV += 12;
            srcLine += 8;
        }
    }

    // 单独处理最后一行,避免循环内分支
    if (height > 0) {
        int lineNo = height - 1;
        const uint32_t* srcLine = reinterpret_cast<const uint32_t*>(src + lineNo * srcStride);
        uint16_t* dstLineY = reinterpret_cast<uint16_t*>(dstY + lineNo * width * 2);
        uint16_t* dstLineUV = reinterpret_cast<uint16_t*>(dstUV + lineNo * width * 2);

        for (int g = 0; g < groupsPerLine; ++g) {
            __m256i dwords = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(srcLine));
            __m256i s0 = _mm256_slli_epi32(_mm256_and_si256(dwords, mask10), 6);
            __m256i s1 = _mm256_slli_epi32(_mm256_and_si256(_mm256_srli_epi32(dwords, 10), mask10), 6);
            __m256i s2 = _mm256_slli_epi32(_mm256_and_si256(_mm256_srli_epi32(dwords, 20), mask10), 6);
            
            __m256i all_components = _mm256_packus_epi32(_mm256_packus_epi32(s0, s1), s2);
            __m256i y_vals = _mm256_permutevar8x32_epi32(all_components, y_perm_mask);
            __m256i uv_vals = _mm256_permutevar8x32_epi32(all_components, uv_perm_mask);
            
            // 最后一组按需处理,避免溢出
            if (g == groupsPerLine - 1) {
                _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineY), _mm256_castsi256_si128(y_vals));
                _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineUV), _mm256_castsi256_si128(uv_vals));
            } else {
                _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineY), _mm256_castsi256_si128(y_vals));
                _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineY + 6), _mm256_extracti128_si256(y_vals, 1));
                _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineUV), _mm256_castsi256_si128(uv_vals));
                _mm_storeu_si128(reinterpret_cast<__m128i*>(dstLineUV + 6), _mm256_extracti128_si256(uv_vals, 1));
            }
            
            dstLineY += 12;
            dstLineUV += 12;
            srcLine += 8;
        }
    }
}

内容来源于stack exchange

火山引擎 最新活动