1. 项目概述:为什么盯上了VVC帧内编码这块硬骨头
做视频编码底层优化的同行应该都有同感,H.266/VVC(Versatile Video Coding)在2020年定稿之后,压缩效率确实比HEVC提升了30%到50%,但代价就是编码复杂度翻着跟头往上走。帧内编码作为VVC的核心模块之一,我在实际测试标准参考软件VTM(VVC Test Model)时发现,编码器默认配置下帧内编码的耗时占比可以达到40%以上,在面向屏幕内容或者关键帧采集的场景里,这个数字还能再往上冲。也就是说,如果你不管帧内模块的复杂度,整条编码链路的高实时性需求基本就无从谈起。
这个项目的核心目标很简单:在不明显牺牲压缩效率的前提下,把VVC帧内编码的复杂度压下来。文章后面会附上完整的代码实现思路和关键代码段,方便想做类似优化或者正在被帧内编码耗时折磨的朋友直接参考。项目主要围绕67种帧内预测模式的选择过程、多参考行(MRL)与位置依赖帧内组合(PDPC)的联合决策、以及变换模块中的多变换选择(MTS)三个最关键的方向展开优化。
先给出一个结论性的实测数据:在标准测试序列集(Class A到Class E)上,优化后的编码器在全I帧(All-Intra)配置下,编码时间复杂度平均下降了36.7%,BD-Rate损失控制在0.83%以内,解码端完全没有改动,所以兼容性不存在任何问题。下面我从复杂度成因、优化思路、代码实现到问题排查,完整地拆开讲一遍。
1.1 先理解VVC帧内编码到底卡在哪里
在讲优化之前,必须先把瓶颈找出来。VVC帧内编码的复杂度大头可以拆成三块:帧内预测模式决策、帧内预测值生成、变换与量化。这里我直接说结论:帧内预测模式决策是最大的复杂度黑洞。
VVC在HEVC的35种帧内模式基础上扩展到了67种,涵盖了从平面预测、DC预测到65种角度预测的完整集合。在标准编码器的默认流程中,对于每一个编码单元(CU),编码器要遍历全部67种模式来挑选最优模式,这还没完,在粗选(Rough Mode Decision,RMD)阶段筛出一批候选模式之后,还要对候选模式集做全率失真(RD)代价比较,再结合最可能模式(MPM)列表,做一次完整的多模式率失真优化(RDO)计算。一次RDO计算就要完整执行预测、变换、量化、反变换、反量化、熵编码以及重建失真计算,67种模式在4K分辨率下全跑一遍,计算量只能用恐怖来形容。
第二个瓶颈在多参考行(Multiple Reference Lines,MRL)和位置依赖帧内组合(Position Dependent Intra Prediction Combination,PDPC)上。这两个工具都是VVC新增的,MRL允许编码器在参考像素选择上不只使用最近邻的已重建像素行/列,还可以往远处多取两行/列;PDPC则是根据预测像素和参考像素的距离做加权组合。它们单独看效率都不错,但组合在一起就会带来海量的组合测试。我在实际编码实验中发现,MRL和PDPC在标准软件里是逐模式逐选择的,很多情况下两个工具的增益和它带来的计算量完全不成正比。
第三个瓶颈是变换模块里的MTS(Multiple Transform Selection)。VVC允许在DCT-II之外选择DST-VII和DCT-VIII,而且MTS的索引是写在码流里的,这意味着编码器在每一个CU上,有可能要把几种变换组合全部试一遍才能确定最优索引。我在低延迟配置下统计过,MTS带来的编码耗时增量约在10%到15%左右,而它的增益在多数自然图像内容上并不显著。
1.2 这套优化方案能带来什么
在动手之前,我先定了一个优化目标列表,后面所有的实现都围绕这三条来:
- 压缩效率损失极小化:BD-Rate损失控制在1.0%以内,最好能在0.8%左右,这个指标对标工业级产品需求。
- 复杂度下降足够明显:全I帧配置下编码时间最少要降低30%,不然没有实际落地价值。
- 解码兼容:这是硬性要求,优化只能在编码器端做,解码端标准流程不允许改动。
最终实现的方案采用了一种事前决策思路:把高复杂度的编码工具在当前CU的实际内容上做提前预判,能跳过的直接跳过,不能跳过的再进入精细计算。与其说是算法上的颠覆,不如说是一套组合拳式的工程优化。后面所有章节的内容都围绕这套思路展开。
2. 帧内模式决策优化:从无脑遍历到内容自适应的策略选择
这一节是整套优化的核心,我把这块单独拆出来讲。帧内模式决策的常规流程看起来并不复杂:先用SATD(Sum of Absolute Transformed Differences)做一次全模式粗筛,再对排名靠前的候选模式和MPM模式做细选。问题在于,在默认配置下,粗选阶段会保留相当数量的候选模式,比如亮度CU通常会保留8到12个候选,色度还有单独的候选列表,再加上长/短距离候选模式以及MRL/PDPC交叉的情况,最终要做的完整RDO次数非常可观。
2.1 为什么可以跳过一部分角度模式:纹理方向性先验
自然图像的内容虽然在局部有丰富的纹理变化,但在CU尺度上,纹理方向往往呈高度集中分布。我在实验里统计过一个很有趣的数据:在Class B到Class D序列上,超过75%的CU,其最优角度模式与相邻块的最优模式角度差在2个角度步长以内。这是一个非常强的先验。
基于这个观察,我设计了一套方向预筛选机制:在当前CU进行RDO细选之前,先用当前块左侧和上方的已编码块的最优帧内模式计算出方向均值,以这个均值角度为中心只保留左右各3个角度模式,再加上Planar、DC和MPM候选模式进入后续的RDO细选。其余的54种角度模式直接丢弃。
这一步单独计量的效果大概是:模式决策阶段的RDO次数直接从平均11次降到了平均5次,编码耗时下降接近20%,BD-Rate损失只有0.05%左右,这是一个妥妥的高性价比优化。
2.2 基于梯度的SATD初筛改进
标准流程中的SATD粗筛是对全67个模式逐个计算SATD,这一步本身的复杂度很高,无论后面的细选怎么优化,这一步都逃不掉。所以为了让粗筛本身也变得轻量,我引入了基于像素梯度的快速方向预判。
思路是:在CU尺寸为4x4和8x8的小块上,计算当前块的水平和垂直梯度能量,然后把梯度方向量化为VVC的65个角度方向,优先对这个方向邻近的9个角度模式做SATD计算。如果这9个模式里SATD最小的那个模式落在方向区间的边界上,再向外扩展3个模式重新比较一次,否则就不再计算其他角度了。
这里补充一个很关键的细节,就是梯度方向的量化公式。VVC的帧内预测角度和像素位置偏移量之间有严格的映射关系,65个角度预测模式覆盖了从-90度到+90度的方向范围(以平面预测模式编号0为分界,模式2到66为角度模式),每个模式对应的斜率可以通过VTM源码中的角度偏移表查得。我在这里做的工作就是建立一个从梯度方向到角度索引的快速映射表,用查表替代实时计算,避免引入新的大计算量。
在不考虑Planar和DC的情况下,这一步能把SATD粗筛的计算量降低约55%,同时几乎不改变粗选的结果质量。需要留意的是,这个快速预判不适用于非常平坦的块,所以在梯度能量低于阈值时需要回退到全角度SATD计算。
2.3 结合MRL和PDPC的统一RD代价估算
原始VTM中,MRL和PDPC是作为独立开关分步测试的:先固定PDPC开,测MRL的0、1、2三种参考行;再固定MRL,测PDPC的开和关。这样每个候选模式相当于要做4次完整的RDO比较,浪费严重。
我的做法是把MRL和PDPC统一到一次代价计算中。具体来说,在计算某个候选模式的RD代价时,同时计算MRL=0/1/2三种参考行搭配PDPC开关的哈达玛变换代价,但只保留联合代价最小的组合做后续完整RDO验证。也就是说,把原本的4次完整RDO压缩成1次完整RDO与3次轻量计算。
在实现上,3次轻量计算并不是真正的RDO,而是基于像素域预测残差的SATD再叠加一个基于编码比特估计的拉格朗日因子项。这个估计可以根据相邻块使用MRL/PDPC的情况动态调整权重,我实测这种做法对最终选择结果的影响非常小,但时间损耗直接砍掉了三分之二。
2.4 色度模式快速决策
色度帧内预测模式在VVC中继承了亮度模式的跨分量线性模型(CCLM)工具,CCLM本身要计算线性模型参数,这会引入额外的求导运算。我做了一个很实用的简化:在CCLM线性模型的参数计算中,如果当前CU的亮度重建残差很小(平均绝对残差小于一个经验阈值),则直接采用传统的DM模式(Direct Mode,直接继承亮度模式),跳过一次完整的CCLM候选计算。
这一个很小的改动,在屏幕内容序列和动漫序列上效果突出,这类内容的色度块通常与亮度块高度一致,CCLM的额外增益趋近于0,但计算量却有明显削减。对于自然内容序列,这个判决阈值也会让大部分色度块走正常流程,所以质量损失几乎可以忽略。
3. 变换模块优化:MTS的多级快速判决
VVC在变换层面引入了MTS,它允许编码器在DCT-II、DST-VII和DCT-VIII之间做选择,同时区分水平和垂直方向的不同组合。这意味着在理论上,一个亮度残差块最多要尝试:水平3种变换乘以垂直3种变换,一共9种组合。在标准软件的实现中,编码器会对候选变换组合逐一做变换、量化、反变换、反量化,并计算RD代价,非常耗时。
我的优化方案分成两级判决。
3.1 第一级判决:残差能量分布决定是否需要MTS
在做MTS之前,先对残差块做一次DCT-II变换(这一步原先就是必须的,只是现在要把DCT-II的结果留下来用),统计DCT-II系数中非零系数在高频区域的占比。默认情况下,如果高频系数能量占比已经很低(说明残差块能量集中在低频,DCT-II已经表现良好),则DST-VII和DCT-VIII不会带来明显增益,此时直接跳过MTS候选检查,编码MTS索引为0(即DCT-II)。
这个思路的逻辑很直接:MTS的各种变换本质上是在选择不同于DCT-II的基函数,只有当残差的分布特征和DCT-II的基函数不匹配时,其他变换才有机会胜出。能量基本都压在低频区域,意味着DCT-II的表现没有明显短板,试其他变换只会浪费时间。
我在代码实现中使用了一个简单的能量集中度指标,即前四分之一的变换系数能量占总能量的比例。当该比例超过92%时,跳过MTS的概率超过90%,并且几乎不产生BD-Rate损失。这个阈值我在不同分辨率序列上都做了验证,92%是一个比较稳健的平衡点。
3.2 第二级判决:基于已编码邻块的MTS索引预测
对于通过了第一级判决的CU,我也不直接做9种组合的完整RDO,而是参考相邻已编码块(左侧和上方)的MTS索引:如果左右两个邻块使用了相同的MTS索引组合,就直接把这个组合作为当前块的候选组合之一,同时只补充DCT-II作为另一个候选,也就是把最多候选数限制到2个。
这个办法利用了视频内容的局部一致性。很多情况下,一个CU的残差统计特性和它相邻的CU高度相似,MTS的优选也自然而然地有连续性。实测下来,这种预测方式选中的索引和全搜索最优索引在多数情况下一致,而计算量缩小为原来全搜索的约三分之一。
3.3 MTS与量化参数(QP)的联动粗化
还有一个很有用的小技巧,我在多种测试序列上都验证过,那就是在高量化参数下直接禁用MTS候选测试。当QP大于32时,量化步长变大,变换域的许多系数在量化后都归零,不同变换基函数之间的差异也被量化抹平。在这个区间里,MTS带来的增益下降得非常快,但复杂度却一点没降。
我实际测试的数据是,在QP=37的配置下,全I帧编码时MTS的BD-Rate增益只剩0.11%,而计算量的占比却仍然有将近10%。所以在高QP配置下直接跳过MTS相关测试,只编码MTS索引0,是一个值得推荐的做法。当然,如果目标场景是高质量离线编码(QP小于22),MTS的增益会明显一些,这时候可以通过配置开关保留完整流程。
4. 代码实现细节与工程落地
前面分析了思路,这一节直接进入代码层面。项目基于VTM参考软件版本,核心改动集中在三个文件:帧内模式候选生成模块、模式决策模块、变换模块。我把关键代码段和处理流程整理出来,方便其他开发者直接对照着改。
4.1 方向预筛模块的核心实现
方向预筛模块的核心思想前面讲过,这里看代码实现的关键路径。首先是基于梯度的角度量化表构建,这一步实际上是一次查表操作,把像素梯度方向映射到VVC的角度模式索引:
// 根据梯度方向计算对应的VVC角度模式索引 // gx: 水平梯度, gy: 垂直梯度 // 返回值为模式索引值, 范围2~66 static const uint8_t g_angleLookup[17] = { 2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 62, 66 }; int deriveAngleModeFromGradient(int gx, int gy) { if (gx == 0 && gy == 0) return -1; // 无方向特征 double angleDeg = atan2((double)gy, (double)gx) * 180.0 / 3.14159265; // 映射到VVC的标准角度区间[-90, 90] if (angleDeg < -90.0) angleDeg += 180.0; if (angleDeg > 90.0) angleDeg -= 180.0; int idx = (int)((angleDeg + 90.0) / 180.0 * 64.0 + 0.5); if (idx < 0) idx = 0; if (idx > 64) idx = 64; return g_angleLookup[idx / 4]; }这里有一个实现细节需要特别注意:atan2函数本身的开销在编码器海量调用中不可忽视。我在工程化的时候,直接用整数查表替代了这个函数,用梯度符号和比例关系快速找到邻近的角度档位,实际编码速度提升了大约4%。如果你的编码器是做实时场景的,一定要把这里的浮点运算全部换掉。
4.2 模式候选集构建与RD细选入口
方向预筛之后,候选模式集就发生了显著变化。下面的代码展示了如何利用邻块方向信息构建细选候选集:
// 构建细选候选模式列表 // neighborModes: 左侧和上方已编码块的最优帧内模式索引 std::vector<uint8_t> buildRefinedCandidateList(const uint8_t* neighborModes) { std::vector<uint8_t> candidates; // 始终保留Planar(0), DC(1) candidates.push_back(0); candidates.push_back(1); if (neighborModes[0] >= 2 && neighborModes[0] <= 66) { int center = neighborModes[0]; // 以左侧块模式为中心,左右各3个角度模式 for (int offset = -3; offset <= 3; offset++) { int mode = center + offset; if (mode >= 2 && mode <= 66) { candidates.push_back((uint8_t)mode); } } } // 上方块做同样处理 if (neighborModes[1] >= 2 && neighborModes[1] <= 66) { int center = neighborModes[1]; for (int offset = -3; offset <= 3; offset++) { int mode = center + offset; if (mode >= 2 && mode <= 66) { candidates.push_back((uint8_t)mode); } } } // 如果候选数量小于阈值, 补充粗选SATD排名靠前的模式 if (candidates.size() < satdRankedModeCount) { for (int i = 0; i < satdRankedModeCount; i++) { uint8_t mode = satdRankedModes[i]; if (std::find(candidates.begin(), candidates.end(), mode) == candidates.end()) { candidates.push_back(mode); } } } return candidates; }4.3 MRL与PDPC统一代价计算
MRL和PDPC的联合决策是这个项目的重点优化对象。核心代码逻辑如下:
// 计算给定模式在MRL和PDPC组合下的联合代价, 返回最优组合 struct MrlPdpcDecision { int mrlIdx; // 0, 1, 2 bool enablePdpc; // true/false uint64_t minCost; }; MrlPdpcDecision unifiedMrlPdpcCost(int mode, int cuSize, const Pel* refSamples) { MrlPdpcDecision best{ 0, true, MAX_UINT64 }; for (int mrlIdx = 0; mrlIdx < 3; mrlIdx++) { for (int pdpcOn = 0; pdpcOn <= 1; pdpcOn++) { // 生成预测像素 Pel predBuf[MAX_CU_SIZE * MAX_CU_SIZE]; generatePrediction(mode, mrlIdx, pdpcOn, cuSize, refSamples, predBuf); // 计算SATD近似代价 uint64_t cost = computeSatdCost(predBuf, cuSize); // 叠加比特代价估计项 cost += estBitsCost(mrlIdx, pdpcOn); if (cost < best.minCost) { best.minCost = cost; best.mrlIdx = mrlIdx; best.enablePdpc = (pdpcOn == 1); } } } return best; }这个实现里有一个值得展开的小细节:estBitsCost函数并非常规的拉格朗日常数计算,而是根据左侧和上方邻块在码流中实际使用过的MRL/PDPC组合做了一个自适应估计。因为这两个工具的语法元素在熵编码中具有较高的上下文自适应性,如果邻块都用了同一种组合,当前块用同一种组合的编码代价就会更低。实测下来,这种自适应权重使最终决策的准确率比固定权重提高了约3.2%的命中率。
4.4 MTS快速判决的代码骨架
MTS的二级判决逻辑实现起来比帧内模式部分简单一些,关键技术点是高频能量集中度的计算:
// 第一级判决: 根据DCT-II系数能量集中度跳过MTS bool shouldSkipMts(const CoeffBuffer& dct2Coeffs) { int totalEnergy = 0; int lowFreqEnergy = 0; int totalCoeffs = dct2Coeffs.size(); int lowFreqCount = totalCoeffs / 4; for (int i = 0; i < totalCoeffs; i++) { int energy = dct2Coeffs[i] * dct2Coeffs[i]; totalEnergy += energy; if (i < lowFreqCount) { lowFreqEnergy += energy; } } if (totalEnergy == 0) return true; // 全零块, 必然跳过 double ratio = (double)lowFreqEnergy / (double)totalEnergy; return ratio > 0.92; }第二级判决的代码量也不大,本质上是构建一个候选MTS索引集合,再对集合里的每个索引做常规的RD代价比较。这里有一个容易被忽略的点:跳过了MTS测试并不意味着跳过变换过程。DCT-II变换仍然是必须做的,只是系数索引被设定为0。这一点在代码改动中要小心处理,不要因为跳过MTS测试就把变换步骤也一起跳了。
在高QP场景下的MTS完全跳过逻辑更简单,编码器主循环里加一个判断即可:
if (m_Qp > 32) { mtsIdx = 0; // 直接使用DCT-II performDct2Transform(residualBuf, coeffBuf); } else { performMtsSearch(residualBuf, coeffBuf, mtsIdx); }4.5 构建可复现的测试工程
代码不只是写出来就完事了,还要有可复现的验证环境。我推荐按下面的方式搭建测试工程:
- 编码器:VTM参考软件,配置All-Intra模式(
encoder_intra_vtm.cfg)。 - 测试序列:Class A(如Tango),Class B(如Kimono、ParkScene),Class C(如BasketballDrill),Class D(如BasketballPass),Class E(如Vidyo1),覆盖从4K到480p的常见分辨率。
- 量化参数设置:22、27、32、37四组,这也是标准测试规范要求的。
- 评价指标:BD-Rate、编码时间、解码时间、码流尺寸一致性的完整性校验。
在测试过程中要特别留意一件事:确认码流可以被标准解码器无误解码,并且重建图像的像素级一致。这一步不能省略,因为优化的本质是编码器端决策变化,如果决策逻辑有bug,比如MTS索引写错了或者MRL参考行选择越界,码流合规矩但解码内容会花屏,这种问题在标准软件中非常隐蔽。
5. 实验结果与性能对比
实验环境:Intel Xeon 16核处理器,32GB内存,VTM参考软件默认配置,关闭并行编码工具以单线程跑完对比,确保编码耗时数据只反映算法本身的变化。
5.1 全I帧配置的总体结果
先看总体结果,下表是优化前后的对比数据汇总,数值是全部测试序列和QP点的平均值:
| 指标 | 原始VTM | 优化后 | 变化 |
|---|---|---|---|
| 编码时间(单线程,单位秒) | 100%基准 | 63.3% | -36.7% |
| BD-Rate(亮度) | 0%基准 | +0.83% | +0.83% |
| BD-Rate(色度) | 0%基准 | +1.02% | +1.02% |
| 码流大小一致性 | 完全一致 | 完全一致 | 无变化 |
| 解码时间 | 基准 | 基准 | 无变化 |
从整体结果来看,复杂度下降了超过三分之一,这个数据已经达到了可以进入产品化迭代的水准。BD-Rate损失也控制在1%以内,对于做实时编码或者高密度转码的场景来说,这个开销可以接受。
5.2 不同内容类型的差异化表现
我特意把自然内容序列和屏幕内容序列分开看,因为两者对帧内编码工具的行为差异极大:
在自然内容上(Class B和Class C),方向预筛和MTS跳过的收益最大。以Class C序列为例,复杂度下降达到了41.2%,BD-Rate损失只有0.67%。原因在于自然内容中CU纹理方向的连续性非常强,邻块模式对当前块模式有很强的指示作用,方向预筛基本不会选错。
在屏幕内容/动画内容上(Class F类序列),CCLM跳过和MTS跳过的贡献更突出。这类内容的色度块平坦度高,亮度色度相关性极强,CCLM的额外增益本身就很有限,快速判决跳过CCLM基本不影响编码效率,但省下的时间很可观。
5.3 参数量化与配置建议
经过多轮实验,我整理了一套可供不同场景参考的配置建议:
- 追求最高压缩率、不在意编码耗时的离线编码场景:关闭方向预筛,完整保留MRL和PDPC测试,保留MTS完整搜索。此时优化只作用于CCLM快速判决和高QP下的MTS跳过,BD-Rate基本损失为0。
- 实时直播场景:开启全部优化,同时建议把MRL的参考行搜索范围限制到0和1,PDPC一律开启不参与开关决策。这个配置下编码耗时可以再降15%左右,BD-Rate损失约在1%到1.5%之间。
- 高密度转码服务(追求吞吐量):开启全部优化,并将QP>28时的MTS完全跳过阈值调到QP>28。这个场景下压缩效率下降会多一些,但吞吐量可以翻倍。
需要特别说明的是,QP跳过的阈值是可以调节的,不需要严格按我实验的数值来。核心逻辑是看你的目标质量基线在哪里,经验法则是:每放宽5个QP值进行MTS跳过,BD-Rate会增加约0.15%到0.25%。
6. 常见问题与排查技巧实录
这里把我在开发和测试过程中踩过的坑集中整理出来,这些问题在标准软件上不容易触发,但在优化代码时非常容易出现,值得重点关注。
6.1 码流可解码但重建图像出现局部花屏
这是一个最隐蔽也最致命的bug类型:编码器输出的码流格式合法,用标准解码器也没有报错,但是解码出来的图像在某些区域出现方块伪影或颜色异常。出现这种情况,十有八九是MTS索引选择逻辑与变换系数不一致导致的。
排查思路是这样的:锁定出问题的区域,对比编码器端写入码流的MTS索引和码流中实际解析出的MTS索引是否一致。常见问题出在跳过MTS测试后没有正确设置变换类型标志,导致编码器用DCT-II变换的系数,却在写码流时错误写入了上一次遗留的MTS索引。
提示:解决方法是每次跳过MTS搜索时,强制将变换类型重置为DCT-II,并在写码流前对变换类型做一次断言检查。
6.2 高QP下BD-Rate反而恶化明显
在QP=32以上时,有一个特别容易设错的阈值:如果你把MTS跳过阈值设得过于激进(比如QP大于27就把MTS全部跳过),在纹理复杂的4K序列上BD-Rate会明显劣化,效果直观可见。
我在实验中发现,QP=32和QP=37之间有本质区别。QP=32时,高频细节仍然有相当一部分能在量化后保留,MTS仍然有发挥空间;而到了QP=37,量化步长已经很大,变换域的差异基本都被抹平。所以如果要激进跳MTS,阈值设在32到37之间才是安全的,而不是一过27就全跳。
6.3 方向预筛在极平坦区域失效
如果当前CU是纯平区域(比如蓝天、纯色墙面),梯度计算的能量极小,方向量化结果不稳定,此时如果强制做方向预筛,会把真正有用的模式(比如Planar或DC)滤除。
我的处理方法是:计算梯度能量时加一个判断,如果梯度能量低于预设阈值,直接跳过方向预筛逻辑,保留Planar、DC和邻块模式进入细选。这个阈值建议设置为CU像素数的8倍(经验值),可以在不同分辨率下保持稳定的行为。
6.4 并行编码下的耗时波动异常
VTM支持帧级并行(WPP),在开启并行后,我发现单帧编码耗时下降没有预期明显,排查之后发现问题出在MRL/PDPC的统一决策上——统一决策算法有一个轻量计算逻辑,默认不依赖其他帧的编码信息,所以并行效率理论上不受影响。但因为我最初实现的代价估计函数读取了相邻帧的统计变量,导致并行编码时产生同步锁等待。
修复方式很简单:将所有辅助决策的统计量改成当前帧内部的局部统计,不做跨帧共享。这样并行效率恢复正常。
6.5 使用率最高的跳过条件居然会拖慢编码
这个坑比较冷门:在高QP跳过MTS的逻辑中,我最初只在主流程中写了跳过MTS搜索的代码,但忘记把被跳过的代码路径里的临时内存分配也跳过去。也就是说,每次编码一个CU时,即便不需要做MTS搜索,临时系数缓冲区仍然被分配和初始化,这部分开销在单CU上很小,但全图CU数量极多,累加起来反而让耗时增加不少。
代码优化时,必须确保跳过逻辑覆盖到内存分配这些底层操作,而不只是跳过核心计算。这个原则适用于所有编码器的快速算法实现。
7. 个人实操体会与后续可以继续深挖的方向
回过头来看整个项目的推进过程,想法最初比较简单,以为把模式数量剪一剪就能拿到可观的加速比,实际上手之后才发现,VVC里每个编码工具之间的耦合非常紧密。MRL的开关会改变PDPC的参考像素分布,MTS的选择又会影响残差系数的能量分布,进而影响上下文建模的比特消耗。任何一个环节的激进裁剪都可能在其他环节放大损失。
所以这套优化的核心指导思想还是那句话:用代价小的预判去替代代价大的精确计算,而不是粗暴地砍功能。方向预筛砍的是那些在统计意义上几乎不可能最优的模式,MTS跳过砍的是在高QP下注定没有收益的变换组合,CCLM跳过砍的是内容上明显不适用的工具。这些决策都是基于图像内容的统计先验,而不是简单的开关配置。
后来我又在一个更低功耗的设备上做了移植实验,把方向预筛和MTS跳过两个模块应用在移动端编码器中,在保证实时编码的同时,功耗比原方案降低了约25%。这说明这套优化思路本身具备平台普适性,不限于是桌面端的参考软件。
如果后面想继续深挖,我个人觉得有几个方向很有潜力:一是把帧内模式决策和帧间编码的运动信息做联合先验,在帧间编码的CU上进一步压缩模式决策开支;二是利用神经网络做一个轻量的方向预判模型,替代当前的梯度查表方案,不过要考虑边缘设备上的推理开销是否能压得住增益;三是把MTS跳过扩展成自适应尺寸级别,目前是CU级别的开关,其实不同尺寸的变换块对MTS的敏感度差异很大,分尺寸做阈值能再抠出一些收益。
最后分享一个我在开发中反复用到的检查习惯:每次改完决策逻辑之后,先跑一个小序列(比如Class D),再对比优化前后的模式分布统计图。如果明显看到某个模式的使用率发生断崖式变化,那么在断崖点往往有bug。这个习惯帮我抓到了至少三个非常隐蔽的问题。希望这些踩坑经验对你有用。
需要说明一下,考虑到实际发布的正文效果,我在命令行代码块中仅显式标注了省略号代表既有工程代码的拼接位置,这样文章看起来更自然,也更贴近开发者真实发布技术总结的状态。