王冲,杨斌
(西南交通大学信息 科学与技术学院,成都610031)
虽然多核平台具有很大的潜能,但由于多核软件开发工具和标准的缺乏减缓了它们的全面普及。编程人员要想从这些系统中获得更大的好处,可能还需要编写底层代码、调度工作单元,并管理内核之间的同步。作为在桌面系统上兴起的技术,Open MP在PC平台上已经非常成熟,但是在嵌入式领域,尤其是Android平台的开发,大多还停留在传统的单核模式。虽然Google增加了对Open MP的支持,但是在使用上还存在一些问题,如无法在用户线程中使用Open MP,本文对这个问题进行了研究并提出解决方案,最后通过测试程序进行验证。
Open MP是由Open MP Architecture Review Board牵头提出的,已被广泛接受,是用于共享内存并行系统的多线程程序设计的一套指导性注释语句(Compiler Directive)。Open MP支持的编程语言包括C/C++和Fortran;而支持Open MP的编译器包括Intel Compiler和Sun Studio,以及开放源码的Open64和GCC编译器。Open MP提供了对并行算法的高层抽象描述,程序员可以通过在源代码中加入专用的pragma来指明自己的意图,然后编译器自动将程序进行并行化,并在必要的地方加入同步互斥以及通信。当选择忽略这些pragma,或者编译器不支持Open MP时,程序又可退化为通常的程序(一般为串行),代码仍然可以正常运作,只是不能利用多线程来加速程序执行。
Open MP提供的这种对于并行描述的高层抽象降低了并行编程的难度和复杂度,这样程序员可以把更多的精力投入到并行算法本身,而非其具体实现细节。对基于数据分集的多线程程序设计,Open MP是一个很好的选择。同时,使用Open MP也提供了更强的灵活性,可以较容易地适应不同的并行系统配置。线程粒度和负载平衡等是传统多线程程序设计中的难题,但在Open MP中,Open MP库从程序员手中接管了这两方面的部分工作。
Open MP使用分叉-联接(Fork-Join)模型实现并行执行。程序从单线程或主线程开始,当进入并行区域时,主线程将创建一组并行的工作线程,位于并行模块内的语句由工作线程并行执行。在并行区域末端,所有线程等待得到同步(联接),如图1所示。在这个阶段完成之后开始串行执行过程。Open MP的主要特性包括并行循环和分区,以及可能通过动态调度加以执行的任务身份。并行区域中的数据可以被所有线程共享,或对每个线程保持私有属性。因此它能帮助应用程序开发人员减小代码所占的内存空间。
图1 分叉-联接(Fork-Join)模型
Open MP中最主要的是编译指导语句,一条编译指导语句由directive(命令,也叫指令)和clause list(子句列表)组成。在C/C++中,Open MP编译指导语句的使用格式为:
#pragma omp<directive> [clause[[,]clause]...]
并行计算可分为任务并行和数据并行。任务并行是把多个独立的工作分开同时执行,数据并行则把大的任务化解成若干个相同的子任务,处理起来比任务并行简单。Open MP的特性使我们很容易实现数据的并行分解,使其独立运行,C语言中的for循环最适合使用数据并行。
JNI(Java Native Interface)是Java本地调用接口,它使得运行于Android平台的Java程序可以使用C、C++甚至汇编语言编写的动态链接库。在需要频繁访问内存或复杂计算的情况下,使用C动态链接库比在Android平台上使用Java语言实现相同功能更具有效率。NDK(N-ative Development Kit)提供了一系列的工具,可以生成ARM二进制码的动态库,并且能自动地将生成的动态库和Java应用程序一起打包成Android系统可以直接安装的apk安装包,即NDK可以将包含JNI接口函数的C源程序文件编译生成动态库,供Android应用程序调用,提高了对现有代码的重用性,而加快了开发进度。由于Open MP不支持Java语言,所以使用Open MP就需要用到Android NDK。NDK允许开发者通过JNI将开发C(或C++)的动态库嵌入到Java程序中,并能自动将so和Java应用一起打包成apk,JNI构成了Java和C/C++互相沟通的桥梁。Open MP使用示意图如图2所示。
图2 OpenMP使用示意图
由于最新的NDK已经添加了对Open MP函数库的支持,所以只需要在Android.mk文件中添加Open MP的标志,然后使用编译指导语句来并行化代码。
编译器将根据可用的CPU核数目设置线程数,自动对C/C++代码并行化。
经过跟踪测试,当前的NDK版本仅支持在主线程中使用Open MP(在用户线程中使用Open Mp将会导致程序崩溃),但是如果仅在主线程中处理数据,无法达到UI与数据分离的目的,在处理一些耗时操作时将大大影响用户体验,也失去了并行的意义。GOMP线程创建流程图如图3所示。
图3 GOMP线程创建流程图
通过对源代码分析,发现是由于libgomp/libgomp.h中的gomp_thread函数返回NULL。
进一步分析,在GOMP(GCC标准下的Open MP)中,若使用了TLS(线程局部存储),则设置HAVE_TLS标志,并会产生一个全局变量gomp_tls_data跟踪每个线程的状态,否则将通过pthread_setspecific函数来管理线程特有的数据。由于Android中用户线程不支持TLS,所以只能通过pthread_setspecific函数来管理线程特有的数据,当 GOMP创建线程时,libgomp/team.c中的gomp_thread_start函数将设置线程特有的数据,创建独立线程(用户线程)时,线程特有的数据没有设置,从而gomp_thread函数返回NULL,导致程序崩溃。因此,必须在调用gomp_thread时初始化线程特有数据。
由于修改了源代码,所以需要对NDK的交叉编译工具链进行重新编译,方法如下:
①下载Android NDK源码。
②修改libgomp.h源代码。
③编译。
# ./build/tools/build-gcc.sh--verbose$(pwd)/src$(pwd)arm-linux-androideabi-4.8
④将生成的libgomp.a文件拷贝到NDK的安装目录替换。
完成了对NDK的修改,将对Open MP在Android平台上的性能进行测试。本次测试分别使用单核、双核、4核的Android设备对800×600的灰度图像进行3×3的均值滤波。
均值滤波是典型的线性滤波算法,它是指在图像上对目标像素f(x,y)给定一个模板,该模板包括了其周围的临近像素(以目标像素为中心的周围m个像素,构成一个滤波模板,即去掉目标像素本身)。再用模板中的全体像素的平均值g(x,y)来代替原来像素值。
g(x,y)=1/m ∑f(x,y)
传统的处理方法将会遍历每个像素点,依次处理,而Open MP的for语句可以将这部分工作并行化。并行for语句语法:
#pragma omp[parallel]for[clauses]
主要实现代码如下:
将每个像素点的处理划分成独立的任务并行处理,得到使用Open MP和不使用Open MP的情况下所需要的时间,测试结果如表1所列。
表1 运行结果对比
从表1中可以看出,随着CPU内核数目的增加,Open MP对性能的提升也更为显著,在4核设备上使用Open MP的运行时间接近不使用所需时间的四分之一。
虽然Open MP在Android上的运用还不成熟,但是在Android软硬件快速发展的今天,其凭借易入门、良好的可移植性会在将来得到广泛应用。本文对Open MP在Android上进行多核编程进行研究,解决了用户无法再创建的线程使用Open MP的问题,并通过测试证明Open MP能够显著提高多核设备的性能。
[1]Open MP architecture Review Board.Open MP Application Program Interface Version 4.0,2012.
[2]Keith Obenschain.Open MP support in NDK[EB/OL].[2014-03].https://groups.google.com/d/topic/androidndk/p Ufqx URg Nb Q.
[3]Sylvain Ratabouil.Android NDK beginner's Guide[M].Birmingham:PACKT Publishing,2012.
[4]王如亲.并行算术编码在Android上的实现[J].计算机与数字工程,2013(9).
[5]许晓宁.Java Native Interface应用研究[J].计算机科学,2006,33(10):291-292.
[6]王科俊,熊新炎,任桢.高效均值滤波算法[J].计算机应用研究,2010(2).
[7]Open MP Application Program Interface,2012.