作者:同济大学 王晗 林涛
H.264是ITU-T和ISO共同制定的新一代视频压缩标准。同以往的标准相比,在计算精度和一些具体的算法上都有很大的改进。这些改进使得H.264能够提供更高的压缩比和更低的比特率。但是我们应该看到,性能的提升是以更大量的计算作为代价的。尽管我们可以使用MMX、SSE、SSE2等对PC上运行的软件编码器进行优化,使性能提高2-3倍,但面对实时的视频处理或其他一些情况时,编码器还需要更快。随着计算机硬件和软件的不断发展,我们可以利用多处理器或Pentium 4类型的CPU中的超线程技术进行线程级的并行处理从而进一步提高编码器的速度。
要发挥超线程处理器的优势,我们还需要应用程序的支持。而OpenMP应用编程接口等技术实现的多任务、多线程和群集,正好为我们编写多线程程序提供了便利。
一 超线程技术(Hyper-Threading Technology)
通常提高处理器性能的方法是提高主频,加大缓存容量。但是这两个方法因为受工艺的影响在一定的时期有一定的限制。于是处理器厂商希望通过其他方法来提升性能,比如设计良好的扩展指令集、流水线操作、更精确的分支预测算法等。
超线程技术也是一种提高处理器工作效率的方法。简单的说,超线程功能把一颗处理器由内部分成了两个"虚拟"的处理器,而且操作系统认为自己运行在多处理器状态下。这是一种类似于多处理器并行工作的技术,但它只是在一个处理器里面多加了一个架构指挥中心(AS),其实AS就是一些通用寄存器和指针等。两个AS共用一套执行单元,缓存等其他结构,使得在只增加大约5%左右的核心大小的情况下,通过两个AS并行工作提高效率。
图1更清晰地说明了具有超线程技术的单处理器系统(a)和双处理器系统(b)的区别。
可以看到,图1(b)中的双处理器各自独享自己的寄存器,缓存,算术逻辑单元等资源。而图1(a)中超线程的做法是复制一颗处理器的架构指挥中心(Architectural State)变成两个,使得操作系统认为是在与两颗处理器沟通,但这两个架构指挥中心共享该处理器的执行资源(Execution Resources),如运算逻辑单元等。架构指挥中心追踪每个程序或线程的执行状况。如此一来,操作系统把工作线程安排好以后,就分派给这两个逻辑上的处理器执行,而这颗CPU的每个执行单元等于在同样的时间内要服务两个“指令处理中心”,从而减少空闲时间,提高了效率。
在图1(a)使用HTT的处理器中,一个物理处理器被看成两个逻辑处理器。这两个逻辑处理器对操作系统来说和图1(b)中的两个处理器没什么不同。超线程技术让单个CPU可以如同两个CPU那样并行处理数据指令。据Intel方面解释,超线程技术能够提高30%以上的性能。
二 OpenMP
要充分发挥超线程技术的优势,我们还要有支持多线程的程序。
OpenMP是一个便携的可扩展的标准,为程序员提供了一个简单和灵活的接口,可以方便地为共享内存的多处理器平台增加并行机制。计算机硬件、软件和工具制造商,例如Intel,DEC,Silicon Graphics,Kuch & Associates和IBM早在15年前就联合定义了OpenMP标准。OpenMP在所有的架构上都支持使用C/C++和FORTRAN进行共享内存并行编程,包括基于Microsoft WindowsNT 和UNIX 操作系统的构架。OpenMP还使用编译器指令和库函数,帮助并行应用程序员使用C/C++和FORTRAN创建多线程应用。
OpenMP是一组编译指导语句,库函数和环境变量的集合,它能明确的指示编译器在程序的某个地方如何插入线程。
用OpenMP编写的程序在运行时采用fork-join并行执行模式。程序开始是以一个单进程运行,称为执行的主线程。主线程顺序运行到第一个并行块结构时就生成一个线程队,原来的主线程成为线程队的主线程。程序中被并行块包围起来的所有语句(包括块内被调用的子程序)在线程队中并行执行,一直到并行块执行完后,线程队中的线程中止,而主线程继续执行。一个程序中可以定义任意数目的并行块,因此,在一个程序的执行中可以分叉、合并若干次。
1. 指令格式
OpenMP指令是用一个特殊的标识符来标识的c/c++注释。支持OpenMP C/C++的编译器通过命令行参数激活和编译所有的OpenMP编译指令。
#pragma omp directive-name [clause[ [,] clause]......] new-line
在C/C++中指令都要以‘#pragma omp’开始。
2. parallel for 结构
当OpenMP遇到parallel for ,就会产生一个线程组,再把for循环在线程组中分配后来并行的执行。OpenMP会来决定需要产生多少个线程,以及这些线程如何同步,何时终止。我们只需告诉OpenMP哪个循环需要多线程化。
下面一个简单的例子:
#pragma omp parallel for
for (i=0; i < numPixels; i++)
{
pGrayScaleBitmap[i] = (unsigned BYTE)
(pRGBBitmap[i].red * 0.299 +
pRGBBitmap[i].green * 0.587 +pRGBBitmap[i].blue * 0.114);
}
3. parallel sections 结构
sections是一个跟循环无关的指示,负责任务分配。在sections内,包括若干个section.各个section内所包围的程序片段,可由不同的线程并行执行。
#pragma omp parallel sections
{
#pragma omp section
{
TaskA();
}
#pragma omp section
{
TaskB();
}
#pragma omp section
{
TaskC();
}
}
上面例子中,所有的section在线程组中进行分配,一个section只被线程组中的一个线程执行一次,但和其他section是同步执行的。
4. 运行库函数
这些库函数可以用来控制或者查询并行执行的环境。下面列出几个较常用的与执行环境有关的运行库函数。
OMP_SET_NUM_THREADS (整型变量)设置该程序所使用的线程总数
OMP_GET_NUM_THREADS取得该程序所使用的线程总数
OMP_GET_THREAD_NUM 取得当前线程的线程号
OMP_GET_NUM_PROCS 取得系统拥有的可用的处理器数
OMP_SET_NESTED (标量逻辑表达式) 如果该标量逻辑表达式为真,则允许嵌套循环的多级并行方式
5. 环境变量
最重要的环境变量是设置该OpenMP执行时所需的线程总数,即OMP_NUM_THREADS。
可以这样设置该环境变量:set OMP_NUM_THREADS =4。但库函数omp_set_num_threads()的调用会设置一个新的线程数覆盖掉原来的环境变量值。
三 H.264中的并行运算分析
同以往的编码方法类似,H.264仍然是基于运动补偿加变换编码的混合编码方式,继续沿用了以前的优秀技术,但H.264又引入了许多先进、实用的视频编码技术,集中到几个方面:多模式的运动估计,帧内预测,多帧预测,统一VLC,4×4二维整数变换,高达1/8像素精度的运动估计,多帧参考等技术。H.264性能的改进是以增加复杂性为代价而获得的。而单处理器的计算能力还不能满足视频编码的各种不同需求,这使得采用并行方式编码H.264视频成为一个趋势。
如图2所示,在H.264中一个视频序列是由许多的图片组(GOP)组成的,每个GOP又包含很多帧,每帧又可分成几个相互独立的slice,slice又能进一步分解成许多作为运动估计和熵编码基本单元的宏块,宏块又可以再被分成更小的块。所有这些都有可能进行不同级别的并行运算。
1. Frame-Level并行
帧之间进行并行运算需要并行处理的帧之间是相互独立的。在H.264中的帧分为三种类型:I、P、B。一般情况下,I帧不需要参考帧,P帧以他前面的P做参考帧,B帧以它前后的P做参考帧。
通常我们编一个帧序列使用的GOP的结构是:IBBPBBPBBPBBP......P帧做B和后面的P的参考,B不作参考帧。如图3,编码器首先编完第0帧I帧,编第3(P)帧,编完第3帧P帧,它前面的第1、2两个B帧和后面的第6帧P就可以同时进行编码了。可见P帧成了重要点,加快P帧的编码速度就可以为后面更多的帧做好准备,从而减少线程空闲。
2. Slice-Level并行
在H.264编码时,编码器会把一幅图像划分为有限数目的slice,各个slice在标准中具有相对独立的语法结构,同一幅图像的不同slice之间不存在参考关系。可见slice也可作为并行编码的基本调度单位在多线程中并行运算。但是不同的slice的划分对比特率会有明显的影响。
图4给出了一幅图像被划分为不同数目的slice时比特率和信噪比的关系。由图可以看出,要达到相同的编码质量,比特率会随着划分的slice数目的增加而大幅上升。这是因为slice的划分会打断帧中宏块的相关性,当宏块不能利用和另一个slice中的宏块间的相关性来压缩时,压缩的效率就会降低。
划分的slice的数目太少会降低并行处理的效率,但数目太多又会降低压缩的效率,这就需要在应用时根据不同的情况采取不同的折中的办法。
3. Macroblock-Level并行
H.264中,一个宏块编码时要用到与之相邻的左、左上、上、右上位置的宏块,如图5。
* 帧内预测:当前块内的象素的预测值要通过左、左上、上、右上方向的宏块计算得到。
* 运动矢量预测:和上面一样,当前块的运动矢量也是通过与之相邻的左、左上、上、右上方向的块的运动矢量得到。
* 环路滤波:为了使当前块的边缘减小失真,要用与之相邻的左边块和上面块来滤波。
因为各宏块间的这种依靠关系,必须等待相邻的块已经编码完成后才能对当前块进行编码,这里就受到处理顺序的约束。左上的宏块要先处理,其他的块才能编码。但是从图6我们也能看到宏块1和2处理完后,块2右边的块和左下方的块都可以进行处理了。
我们假设在一帧中水平和垂直方向上宏块的个数分别是w和h,MB(i)表示一帧中以光栅扫描顺序的第i个宏块。考虑帧内各块间的从属性,我们可以看到MB(i)和MB(i+w-2)能够同时处理。
有了MB(1)、MB(2)、MB(3)和MB(w+1),MB(w+2)就能进行处理,同时MB(4)也能很好的处理。总的来说,宏块处理的时序安排如图中数字所示:{MB(1)},{MB(2)}, {MB(3),MB(w+1)},{MB(4), MB(w+2)},{MB(5), MB(w+3), MB (2w+1)},……,{MB((h-1)*w), MB(h*w-2)},{MB(h*w-1)}, 和 {MB(h*w)}。
四 小结
利用目前被广泛使用的具有超线程技术的Intel P4 2.4GHz处理器进行实验,很容易看到一段程序在使用OpenMP做并行化处理后和处理前相比,运行时CPU利用率大大提高,由50%提高到95%左右,运行时间大大缩短。H.264作为一个新的视频压缩标准,性能提高的同时带来了计算量的大幅增加。但由上面的分析我们可以看到,整个处理过程中有相当多的地方能够进行并行运算。利用OpenMP对H.264编码器进行并行处理优化,再加上具有超线程技术的处理器的支持,必将会大大提高编码器的性能。
H.264是ITU-T和ISO共同制定的新一代视频压缩标准。同以往的标准相比,在计算精度和一些具体的算法上都有很大的改进。这些改进使得H.264能够提供更高的压缩比和更低的比特率。但是我们应该看到,性能的提升是以更大量的计算作为代价的。尽管我们可以使用MMX、SSE、SSE2等对PC上运行的软件编码器进行优化,使性能提高2-3倍,但面对实时的视频处理或其他一些情况时,编码器还需要更快。随着计算机硬件和软件的不断发展,我们可以利用多处理器或Pentium 4类型的CPU中的超线程技术进行线程级的并行处理从而进一步提高编码器的速度。
要发挥超线程处理器的优势,我们还需要应用程序的支持。而OpenMP应用编程接口等技术实现的多任务、多线程和群集,正好为我们编写多线程程序提供了便利。
一 超线程技术(Hyper-Threading Technology)
通常提高处理器性能的方法是提高主频,加大缓存容量。但是这两个方法因为受工艺的影响在一定的时期有一定的限制。于是处理器厂商希望通过其他方法来提升性能,比如设计良好的扩展指令集、流水线操作、更精确的分支预测算法等。
超线程技术也是一种提高处理器工作效率的方法。简单的说,超线程功能把一颗处理器由内部分成了两个"虚拟"的处理器,而且操作系统认为自己运行在多处理器状态下。这是一种类似于多处理器并行工作的技术,但它只是在一个处理器里面多加了一个架构指挥中心(AS),其实AS就是一些通用寄存器和指针等。两个AS共用一套执行单元,缓存等其他结构,使得在只增加大约5%左右的核心大小的情况下,通过两个AS并行工作提高效率。
图1更清晰地说明了具有超线程技术的单处理器系统(a)和双处理器系统(b)的区别。
可以看到,图1(b)中的双处理器各自独享自己的寄存器,缓存,算术逻辑单元等资源。而图1(a)中超线程的做法是复制一颗处理器的架构指挥中心(Architectural State)变成两个,使得操作系统认为是在与两颗处理器沟通,但这两个架构指挥中心共享该处理器的执行资源(Execution Resources),如运算逻辑单元等。架构指挥中心追踪每个程序或线程的执行状况。如此一来,操作系统把工作线程安排好以后,就分派给这两个逻辑上的处理器执行,而这颗CPU的每个执行单元等于在同样的时间内要服务两个“指令处理中心”,从而减少空闲时间,提高了效率。
在图1(a)使用HTT的处理器中,一个物理处理器被看成两个逻辑处理器。这两个逻辑处理器对操作系统来说和图1(b)中的两个处理器没什么不同。超线程技术让单个CPU可以如同两个CPU那样并行处理数据指令。据Intel方面解释,超线程技术能够提高30%以上的性能。
二 OpenMP
要充分发挥超线程技术的优势,我们还要有支持多线程的程序。
OpenMP是一个便携的可扩展的标准,为程序员提供了一个简单和灵活的接口,可以方便地为共享内存的多处理器平台增加并行机制。计算机硬件、软件和工具制造商,例如Intel,DEC,Silicon Graphics,Kuch & Associates和IBM早在15年前就联合定义了OpenMP标准。OpenMP在所有的架构上都支持使用C/C++和FORTRAN进行共享内存并行编程,包括基于Microsoft WindowsNT 和UNIX 操作系统的构架。OpenMP还使用编译器指令和库函数,帮助并行应用程序员使用C/C++和FORTRAN创建多线程应用。
OpenMP是一组编译指导语句,库函数和环境变量的集合,它能明确的指示编译器在程序的某个地方如何插入线程。
用OpenMP编写的程序在运行时采用fork-join并行执行模式。程序开始是以一个单进程运行,称为执行的主线程。主线程顺序运行到第一个并行块结构时就生成一个线程队,原来的主线程成为线程队的主线程。程序中被并行块包围起来的所有语句(包括块内被调用的子程序)在线程队中并行执行,一直到并行块执行完后,线程队中的线程中止,而主线程继续执行。一个程序中可以定义任意数目的并行块,因此,在一个程序的执行中可以分叉、合并若干次。
1. 指令格式
OpenMP指令是用一个特殊的标识符来标识的c/c++注释。支持OpenMP C/C++的编译器通过命令行参数激活和编译所有的OpenMP编译指令。
#pragma omp directive-name [clause[ [,] clause]......] new-line
在C/C++中指令都要以‘#pragma omp’开始。
2. parallel for 结构
当OpenMP遇到parallel for ,就会产生一个线程组,再把for循环在线程组中分配后来并行的执行。OpenMP会来决定需要产生多少个线程,以及这些线程如何同步,何时终止。我们只需告诉OpenMP哪个循环需要多线程化。
下面一个简单的例子:
#pragma omp parallel for
for (i=0; i < numPixels; i++)
{
pGrayScaleBitmap[i] = (unsigned BYTE)
(pRGBBitmap[i].red * 0.299 +
pRGBBitmap[i].green * 0.587 +pRGBBitmap[i].blue * 0.114);
}
3. parallel sections 结构
sections是一个跟循环无关的指示,负责任务分配。在sections内,包括若干个section.各个section内所包围的程序片段,可由不同的线程并行执行。
#pragma omp parallel sections
{
#pragma omp section
{
TaskA();
}
#pragma omp section
{
TaskB();
}
#pragma omp section
{
TaskC();
}
}
上面例子中,所有的section在线程组中进行分配,一个section只被线程组中的一个线程执行一次,但和其他section是同步执行的。
4. 运行库函数
这些库函数可以用来控制或者查询并行执行的环境。下面列出几个较常用的与执行环境有关的运行库函数。
OMP_SET_NUM_THREADS (整型变量)设置该程序所使用的线程总数
OMP_GET_NUM_THREADS取得该程序所使用的线程总数
OMP_GET_THREAD_NUM 取得当前线程的线程号
OMP_GET_NUM_PROCS 取得系统拥有的可用的处理器数
OMP_SET_NESTED (标量逻辑表达式) 如果该标量逻辑表达式为真,则允许嵌套循环的多级并行方式
5. 环境变量
最重要的环境变量是设置该OpenMP执行时所需的线程总数,即OMP_NUM_THREADS。
可以这样设置该环境变量:set OMP_NUM_THREADS =4。但库函数omp_set_num_threads()的调用会设置一个新的线程数覆盖掉原来的环境变量值。
三 H.264中的并行运算分析
同以往的编码方法类似,H.264仍然是基于运动补偿加变换编码的混合编码方式,继续沿用了以前的优秀技术,但H.264又引入了许多先进、实用的视频编码技术,集中到几个方面:多模式的运动估计,帧内预测,多帧预测,统一VLC,4×4二维整数变换,高达1/8像素精度的运动估计,多帧参考等技术。H.264性能的改进是以增加复杂性为代价而获得的。而单处理器的计算能力还不能满足视频编码的各种不同需求,这使得采用并行方式编码H.264视频成为一个趋势。
如图2所示,在H.264中一个视频序列是由许多的图片组(GOP)组成的,每个GOP又包含很多帧,每帧又可分成几个相互独立的slice,slice又能进一步分解成许多作为运动估计和熵编码基本单元的宏块,宏块又可以再被分成更小的块。所有这些都有可能进行不同级别的并行运算。
1. Frame-Level并行
帧之间进行并行运算需要并行处理的帧之间是相互独立的。在H.264中的帧分为三种类型:I、P、B。一般情况下,I帧不需要参考帧,P帧以他前面的P做参考帧,B帧以它前后的P做参考帧。
通常我们编一个帧序列使用的GOP的结构是:IBBPBBPBBPBBP......P帧做B和后面的P的参考,B不作参考帧。如图3,编码器首先编完第0帧I帧,编第3(P)帧,编完第3帧P帧,它前面的第1、2两个B帧和后面的第6帧P就可以同时进行编码了。可见P帧成了重要点,加快P帧的编码速度就可以为后面更多的帧做好准备,从而减少线程空闲。
2. Slice-Level并行
在H.264编码时,编码器会把一幅图像划分为有限数目的slice,各个slice在标准中具有相对独立的语法结构,同一幅图像的不同slice之间不存在参考关系。可见slice也可作为并行编码的基本调度单位在多线程中并行运算。但是不同的slice的划分对比特率会有明显的影响。
图4给出了一幅图像被划分为不同数目的slice时比特率和信噪比的关系。由图可以看出,要达到相同的编码质量,比特率会随着划分的slice数目的增加而大幅上升。这是因为slice的划分会打断帧中宏块的相关性,当宏块不能利用和另一个slice中的宏块间的相关性来压缩时,压缩的效率就会降低。
划分的slice的数目太少会降低并行处理的效率,但数目太多又会降低压缩的效率,这就需要在应用时根据不同的情况采取不同的折中的办法。
3. Macroblock-Level并行
H.264中,一个宏块编码时要用到与之相邻的左、左上、上、右上位置的宏块,如图5。
* 帧内预测:当前块内的象素的预测值要通过左、左上、上、右上方向的宏块计算得到。
* 运动矢量预测:和上面一样,当前块的运动矢量也是通过与之相邻的左、左上、上、右上方向的块的运动矢量得到。
* 环路滤波:为了使当前块的边缘减小失真,要用与之相邻的左边块和上面块来滤波。
因为各宏块间的这种依靠关系,必须等待相邻的块已经编码完成后才能对当前块进行编码,这里就受到处理顺序的约束。左上的宏块要先处理,其他的块才能编码。但是从图6我们也能看到宏块1和2处理完后,块2右边的块和左下方的块都可以进行处理了。
我们假设在一帧中水平和垂直方向上宏块的个数分别是w和h,MB(i)表示一帧中以光栅扫描顺序的第i个宏块。考虑帧内各块间的从属性,我们可以看到MB(i)和MB(i+w-2)能够同时处理。
有了MB(1)、MB(2)、MB(3)和MB(w+1),MB(w+2)就能进行处理,同时MB(4)也能很好的处理。总的来说,宏块处理的时序安排如图中数字所示:{MB(1)},{MB(2)}, {MB(3),MB(w+1)},{MB(4), MB(w+2)},{MB(5), MB(w+3), MB (2w+1)},……,{MB((h-1)*w), MB(h*w-2)},{MB(h*w-1)}, 和 {MB(h*w)}。
四 小结
利用目前被广泛使用的具有超线程技术的Intel P4 2.4GHz处理器进行实验,很容易看到一段程序在使用OpenMP做并行化处理后和处理前相比,运行时CPU利用率大大提高,由50%提高到95%左右,运行时间大大缩短。H.264作为一个新的视频压缩标准,性能提高的同时带来了计算量的大幅增加。但由上面的分析我们可以看到,整个处理过程中有相当多的地方能够进行并行运算。利用OpenMP对H.264编码器进行并行处理优化,再加上具有超线程技术的处理器的支持,必将会大大提高编码器的性能。