轉載自:http://blog.csdn.net/wutong_login/article/details/7546145
這一次的文章將分析X264的多線程過程,也可以說是并行編碼過程。
1. 編譯并行編碼的x264
從X264的幫助命令行可以看到,添加--threads項可以調整運行的線程數,可是當我完成X264編譯,視圖對手頭的YUV進行編碼的時候,發現在自己的雙核計算機上,只能發揮50%的效率,即使使用--threads n 也無濟于事,提示就是沒有打開pthread支持。Pthreads定義了一套 C程序語言類型、函數與常量,它以 pthread.h 頭文件和一個線程庫實現。【1】
下面就把我在windows上實現pthread版本的X264編譯過程寫作如下:
2009年3月的66版本
1. 從http://sourceware.org/pthreads-win32/ 下載pthread的win32版本,把其中的include和lib加入到VC++的引用目錄中去。
2. 在項目屬性的“C/C++ -> 預處理器 ->預處理器”中加入HAVE_PTHREAD。
3. 在osdep.h文件,緊接著#ifdef USE_REAL_PTHREAD加入
#pragma comment(lib, "pthreadVC2.lib")
引用pthreadVC2.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_encoder_encapsulate_nals( x264_t *h )中,不再作為單獨API出現。而x264_encoder_encapsulate_nals(...)分別被x264_encoder_headers(...)和x264_encoder_frame_end(...)所調用,分別用于封裝參數(sps,pps)和其它數據的碼流。
(2)main函數
從代碼的main()函數開始, 這個函數很簡單,就是讀取參數,然后編碼。到了版本77,相對于66版本而已,增加了參數--preset,用于定義一些預設的參數,究竟是哪個版本引入的可自行考證。在調試程序的時候,可以根據需要選擇預設參數值,如果采用默認狀態,編碼的FPS會比較慢。
現在重點考察編碼函數static int Encode( x264_param_t *param, cli_opt_t *opt ), 在這個函數里,將會使用到X264的API,從代碼帶注釋直接裝貼過來,就不解釋了。

首先,代碼通過x264_encoder_open( param ) 和 x264_picture_alloc( )來初始化編碼器和分配內存功輸入YUV圖像使用。接下來可以看到由兩個注釋隔開的代碼塊,它們的功能如下
/* Encode frames */
while(輸入圖像中的正常編碼幀){
編碼正常的碼流
}
/* Flush delayed frames */
編碼因為B幀而殘余的碼流(在B幀編碼中,需要參考最后一個P幀的那些B幀,這時,輸入幀已經結束,而編碼幀尚未結束)
Encode()最后的代碼是進行編碼器關閉和內存謇恚⑼臣票嗦脛∈亢虵PS等。
(3)幀編碼函數Encode_frame()
在上面的兩個編碼代碼塊中,主體函數是
static int Encode_frame( x264_t *h, hnd_t hout, x264_picture_t *pic )
這個函數將輸入每幀的YUV數據,然后諭涑鰊al包。編碼碼流的具體工作交由API
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 )
來完成,它應該是X264中最重要的函數了。
(4)分析x264_encoder_encode()
首先遇到參考幀調整好書如下,
static inline int x264_reference_update( x264_t *h )
它會在h->frames.reference 保留需要的參考幀,然后根據參考幀隊列的大小限制,移除不使用的參考幀。
然后根據注釋把代碼塊逐個往下分析:
/* ------------------- Setup new frame from picture -------------------- */
/* 1: Copy the picture to a frame and move it to a buffer */
把幀輸入的YUV數據傳入 x264_frame_t *fenc中,然后進行一些碼率控制方式的初始化。
/* 2: Place the frame into the queue for its slice type decision */
把fenc放到slice決定隊列中,也輸入碼率控制的一部分
/* 3: The picture is analyzed in the lookahead */
分析slice類型,具體的類型決定工作將在函數void x264_slicetype_decide( x264_t *h )中處理。
后面做碼率控制分析的時候再詳述。
/* ------------------- Get frame to be encoded ------------------------- */
/* 4: get picture to encode */
去處編碼幀,放置在h->fenc中,并重新設置編碼參數。
/* ------------------- Setup frame context ----------------------------- */
/* 5: Init data dependent of frame type */
根據幀類型設置i_nal_type,i_nal_ref_idc,h->sh.i_type ,如果是IDR幀,重置參考幀隊列。
/* ------------------- Init ----------------------------- */
根據當前幀建立參考幀隊列,當前參考幀按編碼幀類型分別寫在h->fref0和h->fref1中。并整理好他們的排列順序,h->fref0按poc從高到低,h->fref1反之。
/* ---------------------- Write the bitstream -------------------------- */
寫NAL碼流
/* Write SPS and PPS */
寫參數集
/* ------------------------ Create slice header ----------------------- */
初始化slice header參數
/* Write frame */
輸出slice header和slice data
函數最后調用
static int x264_encoder_frame_end( x264_t *h, x264_t *thread_current,x264_nal_t **pp_nal, int *pi_nal, x264_picture_t *pic_out )
來做NAL裝,并且調整編碼器狀態和輸出本幀編碼的統計數據。
(5)static void *x264_slices_write( x264_t *h )
這個函數被x264_encoder_encode()調用作為處理slice header和slice data的編碼,這個函數主要是分出slice group中的一個slice,具體做slice編碼則在
static int x264_slice_write( x264_t *h )
這個函數的代碼塊劃分如下:
step1. 初始化NAL,調用x264_slice_header_write()根據前面的參數設置輸出slice header碼流,
step2. 如果是用CABAC,則初始化其上下文。
step3. 進入宏塊,逐個宏塊編碼:
宏塊編碼重要的是以下兩個函數:
x264_macroblock_analyse( h );
x264_macroblock_encode( h );
其之前的代碼是做宏塊數據的導入,其后的代碼是對編碼數據進行熵編碼,根據slicedata協議寫入碼流,更新coded_block_pattern,處理碼率控制狀態和更新CABAC上下文數據等。代碼分析到宏塊級了,就看看這個基本的編碼單位是怎么被處理的吧。
(6)x264_macroblock_analyse( h )
這個函數就是分析宏塊以確定其宏塊分區模式,對I幀進行幀內預測和對P/B幀進行運動估計就發生在此函數,首先進行亮度編碼,緊接著是色度。同樣來一步步分析其實現。
step1. 進行碼率控制準備,x264_mb_analyse_init()函數的功能包括:初始化碼率控制的模型參數(碼率控制依然基于Lagrangian率失真優化算法,所以初始化lambda系數),把各宏塊分類的Cost設為COST_MAX,計算MV范圍,快速決定Intra宏塊。
step2. 根據h->sh.i_type的類型(I,P,B)來分別計算宏塊模式的率失真代價,代價計算使用SATD方法,【2】中有相關介紹。通過計算SATD可以大致估計編碼碼流,作為宏塊選擇的依據。
隨機取h->mb.i_type == I_8x8的情況來分析,
if( h->mb.b_lossless )
x264_predict_lossless_8x8( h, p_dst, i, i_mode, edge );
else
h->predict_8x8[i_mode]( p_dst, edge );
x264_mb_encode_i8x8( h, i, i_qp );
predict_8x8[i_mode]( p_dst, edge )將進行幀內預測,x264_mb_encode_i8x8( h, i, i_qp )進行DCT編碼和量化,同時進行反量化和逆DCT編碼,以備重建圖像使用。
對于I8x8和I4x4的情況一般會進行分別做3個或15個塊的預測和編碼,留下一個塊在x264_macroblock_encode( h )中再預測編碼,原因是前面的塊將作為后面編碼塊的預測依據。具體說會導致 i_pred_mode = x264_mb_predict_intra4x4_mode( h, 4*idx )的計算值發生變化。
P/B幀的幀間預測將在接下來的代碼段發生,具體的運動估計算法不在詳述,以后將補充X264運動估計分析。
step3. 根據i_mbrd的不同,做一些后續運算。
(7)x264_macroblock_encode( h )
在確定了宏塊分區模式后,在本函數將對I幀剩余的宏塊分區進行預測和編碼,而對P/B幀的運動補償和殘差編碼主要發生在這里。
基本流程分析到這里已經算結束了,在代碼中,會發現宏塊的預測和編碼會散布在不同的函數發生,原因是對率失真優化的要求(對P/B幀)。所以,在X264中參考幀管理,碼率控制,幀間預測和多線程編碼都是比較有趣的探索對象。
3. 多線程代碼分析
(1)文檔解讀
分析完X264的基本架構,來看看多線程發揮力量的地方。X264自帶的多線程介紹文檔是本課題的必讀文檔,它存放在X264的DOC文件夾下。本文描述的大意是:當前的X264多線程模式已經放棄基于slice的并行編碼,轉而采用幀級和宏塊級的并行,原因是slice并行需要采用slice group,會引入而外冗余降低編碼效率。摘抄一段原文如下:
New threading method: frame-based
application calls x264
x264 runs B-adapt and ratecontrol (serial to the application, but parallel to the other x264 threads)
spawn a thread for this frame
thread runs encode in 1 slice, deblock, hpel filter
meanwhile x264 waits for the oldest thread to finish
return to application, but the rest of the threads continue running in the background
No additional threads are needed to decode the input, unless decoding+B-adapt is slower than slice+deblock+hpel, in which case an additional input thread would allow decoding in parallel to B-adapt.【3】
以上的說明意味著,X264采用B幀在編碼時不作為參考幀,所以適宜對其進行并行。
(2)運行狀況分析
先來看看x264_pthread_create被調用的地方,只有這些地方才實實在在的創建了線程。
x264_pthread_create( &h->thread_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*)read_frame_thread_int, h->next_args )

由上圖的運行可以看出,在開啟了--threads 4后。x264_slices_write()可以開啟4個線程同時編碼,而同時存在一個主線程和一個x264_lookahead_thread()線程。x264_slices_write()的優先級為低,原因是調用了
if( h->param.i_sync_lookahead )
x264_lower_thread_priority( 10 );
調低本線程的優先級。read_frame_thread_int()是讀磁盤上的流數據信息,因為I/O和內存的不同步,所以應該分開線程處理。
在x264_encoder_open()中可以找到一下代碼,可以看到對于x264_slices_write()和x264_lookahead_thread()都有被分配了專有的上下文變量,供單一線程使用。
for( i = 1; i < h->param.i_threads + !!h->param.i_sync_lookahead; i++ )
CHECKED_MALLOC( h->thread[i], sizeof(x264_t) );
(3)如何確保按指定線程數來開啟線程編碼?
按打印實驗可以看到,假設使用--threads 4的參數選項,代碼會同時開啟4個x264_slices_write()線程,然后每編完一個幀(前面的一個線程返回后),一個新的被產生出來,使得x264_slices_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 frame */
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_encoder_frame_end( x264_t *h, x264_t *thread_current,x264_nal_t **pp_nal, int *pi_nal, x264_picture_t *pic_out )
{
...
if( h->b_thread_active )
{
void *ret = NULL;
x264_pthread_join( h->thread_handle, &ret );
if( (intptr_t)ret )
return (intptr_t)ret;
h->b_thread_active = 0;
}
...
}
從以上兩個函數的代碼段可以看到,h上下文中保持的線程不會多于4個, x264_pthread_create()根據主線程的調用,創建出x264_slices_write線程,然后thread_oldest被指定并被率控函數判斷重設,當前的線程數還不足4的時候,thread_oldest指向新線程,h->b_thread_active為0,不能進入x264_encoder_frame_end()的相關代碼,主線程繼續循環創建x264_slices_write線程,當線程總數為4,這時thread_oldest指向4個線程中被判斷最快返回的那個,這時h->b_thread_active=1將進入x264_pthread_join(),那樣,該線程就將主線至于阻塞狀態,直至thread_oldest完成,才能重現創建新線程,以此機制,保持指定數碼的編碼線程數。
(4)x264_lookahead_thread()線程的作用
在分析這個線程之前,來看看兩個重要的線程控制函數:
//喚醒等待該條件變量的所有線程。如果沒有等待的線程,則什么也不做。
#define x264_pthread_cond_broadcast pthread_cond_broadcast
//自動解鎖互斥量(如同執行了 pthread_unlock_mutex),并等待條件變量觸發。這時線程掛起,不占用 CPU
時間,直到條件變量被觸發。在調用 pthread_cond_wait 之前,應用程序必須加鎖互斥量。pthread_cond_wait 函數返回前,自動重新對互斥量加鎖(如同執行了 pthread_lock_mutex)。
#define x264_pthread_cond_wait pthread_cond_wait
以下的代碼是X264中x264_lookahead_thread代碼經常阻塞的地方,
**************************代碼段A********************************************
if( h->lookahead->next.i_size <= h->lookahead->i_slicetype_length )
{
while( !h->lookahead->ifbuf.i_size && !h->lookahead->b_exit_thread )
x264_pthread_cond_wait( &h->lookahead->ifbuf.cv_fill, &h->lookahead->ifbuf.mutex );
x264_pthread_mutex_unlock( &h->lookahead->ifbuf.mutex );
}
else
{
x264_pthread_mutex_unlock( &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_frame_list_push()得到滿足的,這里在得到一個輸入的新編碼幀后將發出信號。
slist->list[ slist->i_size++ ] = frame;
x264_pthread_cond_broadcast( &slist->cv_fill );
在代碼段A中,if( h->lookahead->next.i_size <= h->lookahead->i_slicetype_length )條件中,i_slicetype_length表示為了進行slice type的判斷而緩存的幀,它的值有取決于h->frames.i_delay,由代碼的初始化設定值決定(默認為40)。也就是說預存40幀的數值,進行slice type決定用。暫時不詳細分析slice type判斷的具體實現,它的大概思想是根據碼率,GOP和失真狀況的權衡,來進行幀類型選擇,在類似實時通信場合,不允許B幀的使用,也不可能預存那么多幀,這樣的處理沒有意義。
回頭看這里的處理意義,是阻塞線程,等待后續的輸入幀,然后利用處理規則來決定其slice type,為slice編碼準備幀。
(5)宏塊級別的并行
在數據結構x264_frame_t中,有變量x264_pthread_cond_t cv; 該變量分別在下面的兩個函數里被封裝了阻塞和喚醒:
void x264_frame_cond_broadcast( x264_frame_t *frame, int i_lines_completed );
void x264_frame_cond_wait( x264_frame_t *frame, int i_lines_completed );
考查它們被調用的地方,
************代碼B****************from x264_macroblock_analyse( )->x264_mb_analyse_init()
int thresh = pix_y + h->param.analyse.i_mv_range_thread;
for( i = (h->sh.i_type == SLICE_TYPE_B); i >= 0; i-- )
{
x264_frame_t **fref = i ? h->fref1 : h->fref0;
int i_ref = i ? h->i_ref1 : h->i_ref0;
for( j=0; j<i_ref; j++ )
{
x264_frame_cond_wait( fref[j], thresh );
thread_mvy_range = X264_MIN( thread_mvy_range, fref[j]->i_lines_completed - pix_y );
}
}
**************************代碼C************************************from x264_fdec_filter_row()
if( h->param.i_threads > 1 && h->fdec->b_kept_as_ref )
{
x264_frame_cond_broadcast( h->fdec, mb_y*16 + (b_end ? 10000 : -(X264_THREAD_HEIGHT <<h->sh.b_mbaff)) );
}
從上面的代碼段可以看到沒完成圖像一行的編碼,便會使用mb_y*16 -X264_THREAD_HEIGH的值來嘗試喚醒x264_pthread_cond_wait( &frame->cv, &frame->mutex ),要判斷的條件是
mb_y*16 -X264_THREAD_HEIGH < thresh = pix_y + h->param.analyse.i_mv_range_thread;
后者作為一個設想的閾值,用于確保依賴于本幀的后續幀在編碼時,本幀已經編碼出若干行宏塊,以后續編碼幀的基礎,那樣可以設想的情形如下圖,不過X264是以編碼完整行為單位的。

本文的分析道這里告一段落,對于幀間多線程分析和宏塊的并行優化,或按自己的應用做代碼裁剪,可以通過改正上面的(4)(5)代碼段來實現,在當前(四核CPU)的X264測試中,已有代碼確實能夠很好的利用多核資源,并行編碼的話題會隨硬件的升級而不斷探索下去。