如何利用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




