X264的多线程过程
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
X264的多线程过程,也可以说是并行编码过程。
1. 编译并行编码的x264
从X264的帮助命令行可以看到,添加--threads项可以调整运行的线程数,可是当我完成X264编译,视图对手头的YUV进行编码的时候,发现在自己的双核计算机上,只能发挥50%的效率,即使使用--thr eads n 也无济于事,提示就是没有打开pthr ead支持。
Pthr eads定义了一套 C程序语言类型、函数与常量,它以pthread.h头文件和一个线程库实现。
【1】
下面就把我在windows上实现pthr ead版本的X264编译过程写作如下:
2009年3月的66版本
1. 从http://sourcew /pthreads-w in32/下载pthr ead的win32版本,把其中的include和lib加入到VC++的引用目录中去。
2. 在项目属性的“C/C++ -> 预处理器 ->预处理器”中加入HAVE_PTHREAD。
3. 在osdep.h文件,紧接着#ifdef USE_REAL_PTHREAD加入
#pragma c omment(lib, "pthr eadVC2.lib")
引用pthr eadVC2.lib,重新编译。
2009年10月的77版本
4. 在项目属性的“C/C++ -> 预处理器 ->预处理器”中加入SYS_MINGW。
其它版本请自己根据可能的编译错误随机应变。
调整项目属性意味着同时调整libx264和x264两处的属性。
经过如上调整编译出的X264就可以在--threads n //n>=2的时候用完CPU的潜力了。
2. X264的编码基本流程
(1)接口变更
以前曾经写过文章介绍X264的编程架构并且分析了它的接口,现在进一步看看x264是怎么把YUV图像编程H.264编码的。
在代码分析中,最容易让人头疼的是X264代码随处充斥着的多线程处理和码率控制两方面的代码,所以,这里将先简化过程,忽略掉这些非主体代码。
需要说明的是,本文分析的是版本77,2009年10月的版本。
这里的API比版本66少了x264_nal_encode(...),该函数是将码率封装成NAL,现在它被放到static int
x264_enc oder_enc apsulate_nals( x264_t *h )中,不再作为单独API出现。
而x264_enc oder_enc apsulate_nals(...)分别被x264_enc oder_header s(...)和x264_encoder_fr ame_end(...)所调用,分别用于封装参数(sps,pps)和其它数据的码流。
(2)main函数
从代码的main()函数开始, 这个函数很简单,就是读取参数,然后编码。
到了版本77,相对于66版本而已,增加了参数--preset,用于定义一些预设的参数,究竟是哪个版本引入的可自行考证。
在调试程序的时候,可以根据需要选择预设参数值,如果采用默认状态,编码的FPS会比较慢。
现在重点考察编码函数static int E ncode( x264_param_t *par am, cli_opt_t *opt ),在这个函数里,将会使用到X264的API,从代码带注释直接装贴过来,就不解释了。
首先,代码通过x264_enc oder_open( par am ) 和 x264_pictur e_alloc( )来初始化编码器和分配内存功输入YUV图像使用。
接下来可以看到由两个注释隔开的代码块,它们的功能如下
/* Enc ode fr ames */
while(输入图像中的正常编码帧){
编码正常的码流
}
/* Flush delayed frames */
编码因为B帧而残余的码流(在B帧编码中,需要参考最后一个P帧的那些B帧,这时,输入帧已经结束,而编码帧尚未结束)Encode()最后的代码是进行编码器关闭和内存謇恚 ⑼臣票嗦胫∈ 亢虵PS等。
(3)帧编码函数Enc ode_fr ame()
在上面的两个编码代码块中,主体函数是
static int Enc ode_fr ame( x264_t *h, hnd_t hout, x264_pictur e_t *pic )
这个函数将输入每帧的YUV数据,然后谕 涑鰊al包。
编码码流的具体工作交由API
int x264_enc oder_enc ode( x264_t *h,x264_nal_t **pp_nal, int *pi_nal,x264_pictur e_t *pic_in, x264_pictur e_t *pic_out )
来完成,它应该是X264中最重要的函数了。
(4)分析x264_enc oder_enc ode()
首先遇到参考帧调整好书如下,
static inline int x264_refer enc e_update( x264_t *h )
它会在h->fr ames.refer enc e 保留需要的参考帧,然后根据参考帧队列的大小限制,移除不使用的参考帧。
然后根据注释把代码块逐个往下分析:
/* ------------------- Setup new frame fr om pictur e -------------------- */
/* 1: Copy the pictur e to a fr ame and move it to a buffer */
把帧输入的YUV数据传入 x264_fr ame_t *fenc中,然后进行一些码率控制方式的初始化。
/* 2: Plac e the fr ame into the queue for its slic e type decision */
把fenc放到slic e决定队列中,也输入码率控制的一部分
/* 3: The picture is analyzed in the lookahead */
分析slic e类型,具体的类型决定工作将在函数void x264_slic etype_decide( x264_t *h )中处理。
后面做码率控制分析的时候再详述。
/* ------------------- Get fr ame to be enc oded ------------------------- */
/* 4: get pictur e to enc ode */
去处编码帧,放置在h->fenc中,并重新设置编码参数。
/* ------------------- Setup fr ame c ontext ----------------------------- */
/* 5: Init data dependent of fr ame type */
根据帧类型设置i_nal_type,i_nal_ref_idc,h->sh.i_type ,如果是IDR帧,重置参考帧队列。
/* ------------------- Init ----------------------------- */
根据当前帧建立参考帧队列,当前参考帧按编码帧类型分别写在h->fr ef0和h->fr ef1中。
并整理好他们的排列顺序,
h->fr ef0按poc从高到低,h->fref1反之。
/* ---------------------- Write the bitstr eam -------------------------- */
写NAL码流
/* Write SPS and PPS */
写参数集
/* ------------------------ Create slic e header ----------------------- */
初始化slic e header参数
/* Write fr ame */
输出slic e header和slic e data
函数最后调用
static int x264_encoder_fr ame_end( x264_t *h, x264_t *thr ead_current,x264_nal_t **pp_nal, int *pi_nal, x264_pictur e_t *pic_out )
来做NAL装,并且调整编码器状态和输出本帧编码的统计数据。
(5)static void *x264_slic es_write( x264_t *h )
这个函数被x264_enc oder_enc ode()调用作为处理slic e header和slic e data的编码,这个函数主要是分出slic e gr oup 中的一个slic e,具体做slic e编码则在
static int x264_slic e_write( x264_t *h )
这个函数的代码块划分如下:
step1. 初始化NAL,调用x264_slic e_header_write()根据前面的参数设置输出slic e header码流,
step2. 如果是用CABAC,则初始化其上下文。
step3. 进入宏块,逐个宏块编码:
宏块编码重要的是以下两个函数:
x264_macr obloc k_analyse( h );
x264_macr obloc k_enc ode( h );
其之前的代码是做宏块数据的导入,其后的代码是对编码数据进行熵编码,根据slicedata协议写入码流,更新
coded_bloc k_patter n,处理码率控制状态和更新CABAC上下文数据等。
代码分析到宏块级了,就看看这个基本的编码单位是怎么被处理的吧。
(6)x264_macrobloc k_analyse( h )
这个函数就是分析宏块以确定其宏块分区模式,对I帧进行帧内预测和对P/B帧进行运动估计就发生在此函数,首先进行亮度编码,紧接着是色度。
同样来一步步分析其实现。
step1. 进行码率控制准备,x264_m b_analyse_init()函数的功能包括:初始化码率控制的模型参数(码率控制依然基于Lagr angian率失真优化算法,所以初始化lambda系数),把各宏块分类的Cost设为COST_MAX,计算MV范围,快速决定Intr a宏块。
step2. 根据h->sh.i_type的类型(I,P,B)来分别计算宏块模式的率失真代价,代价计算使用SATD方法,【2】中有相关介绍。
通过计算SATD可以大致估计编码码流,作为宏块选择的依据。
随机取h->m b.i_type == I_8x8的情况来分析,
if( h->mb.b_lossless )
x264_predict_lossless_8x8( h, p_dst, i, i_mode, edge );
else
h->pr edict_8x8[i_mode]( p_dst, edge );
x264_mb_enc ode_i8x8( h, i, i_qp );
pr edict_8x8[i_mode]( p_dst, edge )将进行帧内预测,x264_mb_enc ode_i8x8( h, i, i_qp )进行DCT编码和量化,同时进行反量化和逆DCT编码,以备重建图像使用。
对于I8x8和I4x4的情况一般会进行分别做3个或15个块的预测和编码,留下一个块在x264_macr obloc k_enc ode( h )中再预测编码,原因是前面的块将作为后面编码块的预测依据。
具体说会导致 i_pr ed_mode =
x264_mb_pr edict_intr a4x4_mode( h, 4*idx )的计算值发生变化。
P/B帧的帧间预测将在接下来的代码段发生,具体的运动估计算法不在详述,以后将补充X264运动估计分析。
step3. 根据i_mbr d的不同,做一些后续运算。
(7)x264_macr obloc k_enc ode( h )
在确定了宏块分区模式后,在本函数将对I帧剩余的宏块分区进行预测和编码,而对P/B帧的运动补偿和残差编码主要发生在这里。
基本流程分析到这里已经算结束了,在代码中,会发现宏块的预测和编码会散布在不同的函数发生,原因是对率失真优化的要求(对P/B帧)。
所以,在X264中参考帧管理,码率控制,帧间预测和多线程编码都是比较有趣的探索对象。
3. 多线程代码分析
(1)文档解读
分析完X264的基本架构,来看看多线程发挥力量的地方。
X264自带的多线程介绍文档是本课题的必读文档,它存放在X264的DOC文件夹下。
本文描述的大意是:当前的X264多线程模式已经放弃基于slic e的并行编码,转而采用帧级和宏块级的并行,原因是slic e并行需要采用slic e gr oup,会引入而外冗余降低编码效率。
摘抄一段原文如下:
New threading method: fr ame-based
applic ation c alls x264
x264 r uns B-adapt and r atec ontrol (serial to the applic ation, but par allel to the other x264 thr eads)
spawn a thread for this fr ame
thread runs encode in 1 slice, debloc k, hpel filter
meanwhile x264 waits for the oldest thr ead to finish
retur n to applic ation, but the rest of the threads c ontinue running in the bac kgr ound
No additional thr eads ar e needed to dec ode the input, unless dec oding+B-adapt is slower than
slic e+debloc k+hpel, in whic h c ase an additional input thr ead would allow decoding in parallel to B-adapt.【3】以上的说明意味着,X264采用B帧在编码时不作为参考帧,所以适宜对其进行并行。
(2)运行状况分析
先来看看x264_pthread_create被调用的地方,只有这些地方才实实在在的创建了线程。
x264_pthread_create( &h->thr ead_handle, NULL, (void*)x264_slices_write, h )
x264_pthread_create( &look_h->thread_handle, NULL, (void *)x264_lookahead_thread, look_h )
x264_pthread_create( &h->tid, NULL, (void*)r ead_fr ame_thr ead_int, h->next_args )
由上图的运行可以看出,在开启了--thr eads 4后。
x264_slic es_write()可以开启4个线程同时编码,而同时存在一个主线程和一个x264_lookahead_thr ead()线程。
x264_slices_write()的优先级为低,原因是调用了if( h->par am.i_sync_lookahead )
x264_lower_thr ead_priority( 10 );
调低本线程的优先级。
read_frame_thr ead_int()是读磁盘上的流数据信息,因为I/O和内存的不同步,所以应该分开线程处理。
在x264_enc oder_open()中可以找到一下代码,可以看到对于x264_slic es_write()和x264_lookahead_thr ead()都有被分配了专有的上下文变量,供单一线程使用。
for( i = 1; i < h->par am.i_thr eads + !!h->param.i_sync_lookahead; i++ )
CHE CKED_MALLOC( h->thread[i], sizeof(x264_t) );
(3)如何确保按指定线程数来开启线程编码?
按打印实验可以看到,假设使用--threads 4的参数选项,代码会同时开启4个x264_slices_write()线程,然后每编完一个帧(前面的一个线程返回后),一个新的被产生出来,使得x264_slic es_write()线程总数保持在4个,这一过程的相关代码如下:
int x264_encoder_encode( x264_t *h,x264_nal_t **pp_nal, int
*pi_nal,x264_picture_t *pic_in,
x264_picture_t *pic_out )
{
...
if( h->param.i_threads > 1)
{
int i = ++h->i_thread_phase;
int t = h->param.i_threads;
thread_current = h->thread[ i%t ];
thread_prev = h->thread[ (i-1)%t ];
thread_oldest = h->thread[ (i+1)%t ];
x264_thread_sync_context( thread_current, thread_prev );
x264_thread_sync_ratecontrol( thread_current, thread_prev,
thread_oldest );
h = thread_current;
}
...
/* Write fram e */
if( h->param.i_threads > 1 )
{
printf("x264_pthread_create\n");
if( x264_pthread_create( &h->thread_handle, NULL,
(void*)x264_slices_write, h ) )
return -1;
h->b_thread_active = 1;
}
else
if( (intptr_t)x264_slices_write( h ) )
return -1;
return x264_encoder_frame_end( thread_oldest, thread_current, pp_nal,
pi_nal, pic_out );
...
}
static int x264_enc oder_frame_end( x264_t *h, x264_t *thread_curr ent,x264_nal_t **pp_nal, int *pi_nal, x264_pictur e_t *pic_out )
{
...
if( h->b_thr ead_active )
{
void *ret = NULL;
x264_pthread_join( h->thread_handle, &r et );
if( (intptr_t)r et )
r eturn (intptr_t)r et;
h->b_thr ead_active = 0;
}
...
}
从以上两个函数的代码段可以看到,h上下文中保持的线程不会多于4个,x264_pthread_create()根据主线程的调用,创建出x264_slices_write线程,然后thread_oldest被指定并被率控函数判断重设,当前的线程数还不足4的时候,thread_oldest指向新线程,h->b_thr ead_active为0,不能进入x264_enc oder_fr ame_end()的相关代码,主线程继续循环创建x264_slices_write线程,当线程总数为4,这时thread_oldest指向4个线程中被判断最快返回的那个,这时
h->b_thr ead_active=1将进入x264_pthr ead_join(),那样,该线程就将主线至于阻塞状态,直至thread_oldest 完成,才能重现创建新线程,以此机制,保持指定数码的编码线程数。
(4)x264_lookahead_thr ead()线程的作用
在分析这个线程之前,来看看两个重要的线程控制函数:
//唤醒等待该条件变量的所有线程。
如果没有等待的线程,则什么也不做。
#define x264_pthread_c ond_br oadc ast pthr ead_cond_br oadcast
//自动解锁互斥量(如同执行了 pthr ead_unloc k_mutex),并等待条件变量触发。
这时线程挂起,不占用CPU
时间,直到条件变量被触发。
在调用pthr ead_cond_wait 之前,应用程序必须加锁互斥量。
pthr ead_c ond_wait 函数返回前,自动重新对互斥量加锁(如同执行了pthr ead_loc k_mutex)。
#define x264_pthread_c ond_wait pthr ead_cond_wait
以下的代码是X264中x264_lookahead_thr ead代码经常阻塞的地方,
**************************代码段A********************************************
if( h->lookahead->next.i_size <= h->lookahead->i_slic etype_length )
{
while( !h->lookahead->ifbuf.i_size && !h->lookahead->b_exit_thr ead )
x264_pthr ead_cond_wait( &h->lookahead->ifbuf.cv_fill, &h->lookahead->ifbuf.mutex );
x264_pthr ead_mutex_unloc k( &h->lookahead->ifbuf.mutex );
}
else
{
x264_pthr ead_mutex_unloc k( &h->lookahead->ifbuf.mutex );
x264_lookahead_slicetype_decide( h );
}
这里是等待满足!h->lookahead->ifbuf.i_size && !h->lookahead->b_exit_thread 的条件,后一条件在正常编码过程是TRUE,因为不会无故退出线程。
那么这里等待的其实是ifbuf.i_size为非0.查找相关代码,
这里的ifbuf.i_size条件是在x264_synch_fr ame_list_push()得到满足的,这里在得到一个输入的新编码帧后将发出信号。
slist->list[ slist->i_size++ ] = fr ame;
x264_pthread_cond_br oadcast( &slist->cv_fill );
在代码段A中,if( h->lookahead->next.i_size <= h->lookahead->i_slic etype_length )条件中,i_slic etype_length 表示为了进行slic e type的判断而缓存的帧,它的值有取决于h->fr ames.i_delay,由代码的初始化设定值决定(默认为40)。
也就是说预存40帧的数值,进行slice type决定用。
暂时不详细分析slic e type判断的具体实现,它的大概思想是根据码率,GOP和失真状况的权衡,来进行帧类型选择,在类似实时通信场合,不允许B帧的使用,也不可能预存那么多帧,这样的处理没有意义。
回头看这里的处理意义,是阻塞线程,等待后续的输入帧,然后利用处理规则来决定其slice type,为slic e编码准备帧。
(5)宏块级别的并行
在数据结构x264_frame_t中,有变量x264_pthr ead_cond_t cv; 该变量分别在下面的两个函数里被封装了阻塞和唤醒:void x264_fr ame_c ond_broadc ast( x264_fr ame_t *fr ame, int i_lines_c ompleted );
void x264_fr ame_c ond_wait( x264_fr ame_t *fr ame, int i_lines_completed );
考查它们被调用的地方,
************代码B****************fr om x264_macr obloc k_analyse( )->x264_m b_analyse_init()
int thr esh = pix_y + h->par am.analyse.i_m v_r ange_thr ead;
for( i = (h->sh.i_type == SLICE_TYPE_B); i >= 0; i-- )
{
x264_frame_t **fref = i ? h->fr ef1 : h->fr ef0;
int i_r ef = i ? h->i_ref1 : h->i_ref0;
for( j=0; j<i_r ef; j++ )
{
x264_fr ame_c ond_wait( fr ef[j], thr esh );
thr ead_m vy_range = X264_MIN( thread_m vy_r ange, fr ef[j]->i_lines_c ompleted - pix_y );
}
}
**************************代码C************************************from
x264_fdec_filter_row()
if( h->par am.i_thr eads > 1 && h->fdec->b_kept_as_ref )
{
x264_frame_c ond_broadc ast( h->fdec, mb_y*16 + (b_end ? 10000 : -(X264_THREAD_HEIGHT
<<h->sh.b_mbaff)) );
}
从上面的代码段可以看到没完成图像一行的编码,便会使用m b_y*16 -X264_THREAD_HE IGH的值来尝试唤醒
x264_pthr ead_cond_wait( &fr ame->cv, &frame->mutex ),要判断的条件是
mb_y*16 -X264_THREAD_HEIGH < thresh = pix_y + h->par am.analyse.i_m v_range_thr ead;
后者作为一个设想的阈值,用于确保依赖于本帧的后续帧在编码时,本帧已经编码出若干行宏块,以后续编码帧的基础,那样可以设想的情形如下图,不过X264是以编码完整行为单位的。
本文的分析道这里告一段落,对于帧间多线程分析和宏块的并行优化,或按自己的应用做代码裁剪,可以通过改正上面的(4)(5)代码段来实现,在当前(四核CPU)的X264测试中,已有代码确实能够很好的利用多核资源,并行编码的话题会随硬件的升级而不断探索下去。
【参考文献】
1. 维基百科,/wiki/POSIX_Threads
2. MVC学习的第四周小结(2009.6.1-2009.6.7)-运动估计2,http://j m vc.blog.sohu.c om/117738672.htm l
3. X264 document,thr eads.txt。