文章目录
HEVC中的并行处理技术
功能并行和数据并行
此处只讨论多处理器的并行技术,可分为功能并行和数据并行:
- 功能并行,指将不同的功能模块划分给不同的运算单元,因此又叫”流水线级并行“,这种方法充分利用了功能模块的时间并行性,适合硬件实现,缺点是容易产生载荷失衡的问题,而且拓展性较差;
- 数据并行,把信息划分为相互独立的部分,每一部分交给不同的运算单元完成,该种方法具备较好的拓展性,而且如果保证数据单元数大于运算单元数,比较容易达到负载均衡,但是有时候难以保证数据之间的独立性,不得不要进行核间通信来消除数据单元间的依赖性。
HEVC解码端的并行处理框架
因为熵解码在Slice、Tile和CTB行的起始处都有可能进行概率模型初始化,而且不同模型的初始化位置不一定相同,正是因为熵解码之间的依赖关系和后续解码模块的依赖关系不一致,所以需要两个功能并行模块,即熵编码和其余模块解码。
在每个功能模块中使用数据并行分为更多并行模块,每个运算单元负责其中一个模块:
HEVC中编码单元数据之间的依赖关系
HEVC中编码单元数据的依赖关系来源于帧内预测、帧间预测、去方块滤波和样点自适应补偿(SAO)等过程,因为编码/解码当前块必须知道其参考块的编码/解码信息。
四种方式的参考信息如下所示:
HEVC编解码的并行策略
这里其实是很灵活的,可以分为GOP级别、图像级别、Slice级别、Tile级别 和 CTB级别的并行,主要介绍Tile级别的并行策略和CTB级别的并行策略。
Tile级别的并行策略
Tile是H265新增加的数据单元,将原来的图像分为一个个独立的矩形区域,各区域独立进行编码,不会相互参考,很适合于并行化处理。(补充:Tile与之前的Slice划分相互独立,但是要保证一个Tile不会跨过两个Slice,或一个Slice不会跨过两个Tile。而且当多个Tile共用一个Slice的时候,可以公用同一个Slice头,从而可以节省码率。)
Tile中的CTB是按照光栅扫描的顺序进行扫描的,而Tile之间的排序也是按照光栅扫面的顺序排列的,如下图所示:
可见与Slice的条状相比,Tile的矩形表达提高了内部像素的相关性,而且减少了由于运动预测所需要的缓冲数量(即要缓存的运动搜索范围相比Slice的实现要更小了),但是过多的Tile势必降低率失真性能,因为Tile边界附近的信息被破坏了,下面的波前并行处理算法很好的解决了这个问题。
值得说明的是,虽然Tile之间是相互独立的,而且某些熵编码会在Tile的结尾进行上下文模型的更新,但是去方块滤波和样点自适应补偿仍然可能会跨过Tile边界,此时还是需要额外的核间通信。
CTB级别的并行策略/波前并行处理算法(WPP)
波前并行处理算法,可以允许多个CTB同时处理,而且后一行的处理要比前一行滞后两个CTB,这样可以在不破坏边界相关性的前提下进行并行编码。该算法的处理过程如下图所示:
人们还提出了 依赖片 的数据单元,即将一个完整的Slice划分为不同的区域,分别封装到不同的独立NAL中,这些区域可能是相互关联的,因此被称为依赖片。
对应到该算法,可以将一段用波前并行处理的 CTB行 数据或者Tile 数据打包到一个单独的NAL中,这个单元就被称为依赖片。如下图所示
这样的好处是,各个NAL虽然是相互依赖的,但是NAL2不用等待NAL1解码完成再解码,而是可以在NAL1解码的同时进行解码,只需要保证落后两个CTB即可,这样可以大大减少解码时延。
坏处就是,码率会十分不平衡,尤其是当上方的运算单元已经结束,但是下方仍然没有结束,此时码率会逐渐降低,这是我们不希望看到的。于是人们提出"重叠波前并行算法",当上方部分运算单元结束之后,会自动编码下一张图片,而不会继续等待,如下图所示,T1~T4会继续编码之后的帧:
造成的问题是未编码的像素块的运动矢量要尽可能小,在上面的例子中,运动矢量不能大于4。
x265多线程实现原理
main()
函数主要调用了encoder_open()
,encoder_headers()
,encoder_encode()
和encoder_close()
,其中
encoder_open()
除了打印配置信息,还调用了encoder_create()
函数(实际调用了Encoder::create()
),完成了 等待线程的初始化,并进入threadMain()
函数中触发等待线程m_done.wait()
,该事件标志线程初始化;encoder_encode()
如上个博客所说,调用了Encoder::encode()
函数,在encoder
中进一步调用了FrameEncoder::startCompressFrame()
,在startCompressFrame
中触发线程m_done.trigger
,表示进入threadMain()
中。- 该线程接下来调用了
compressSlice
、encodeCTU
、encodeCU
和finishCU
完成编码
多线程算法流程图
Encoder::create()
Encoder::create()
主要用于检查线程池以及可用的线程数目,若符合线程使用条件则调用threadMain()
函数。
调用语句为
for (int i = 0; i < m_param->frameNumThreads; i++)
{
m_frameEncoder[i]->start();
// wait for thread to initialize
m_frameEncoder[i]->m_done.wait();
}
FrameEncoder::threadMain()
这里的FrameEncoder::threadMain()
相当于线程的main函数,主要功能是线程触发后等待处理的过程,主要调用了compressFrame()
函数。
如果当前线程池不为空,调用m_pool->setCurrentThreadAffinity()
设置当前线程m_tld
,如果线程池不为空,创建新的线程m_tld
。
触发两个事件:m_done.trigger()
,线程已初始化完成的信号;m_enable.wait()
,等待Encoder::encoder()
唤醒。
总结如下:
Reference
- 万帅《新一代高效视频编码:原理、标准与实现》
- x265探索与研究 之 x265多线程
视频编码:原理、标准与实现》
2. x265探索与研究 之 x265多线程