摘 要:面對日益復(fù)雜的智能移動應(yīng)用,多核處理器已成為高性能移動計算的一個有效解決方案。對于多核系統(tǒng)中的應(yīng)用軟件性能優(yōu)化也是其中的研究重點。本文研究了并行程序設(shè)計算法和并行程序性能優(yōu)化技術(shù)。通過對程序進(jìn)行優(yōu)化,可以使它充分的發(fā)揮多核的計算能力,其方法包括增加任務(wù)數(shù)量改善負(fù)載均衡,選擇最優(yōu)的線程與處理核之間關(guān)聯(lián)策略,從而能夠大幅提高系統(tǒng)的整體性能。
關(guān)鍵詞:移動智能多核系統(tǒng);多線程;并行計算;分治法;GPA
中圖分類號:TP311.52
由于移動智能系統(tǒng)是一個資源受限的系統(tǒng),它對程序的運行空間和時間要求比桌面系統(tǒng)更為苛刻,因此,應(yīng)用軟件的優(yōu)化對移動智能系統(tǒng)來說尤顯必要和緊迫[1]。
本文主要研究的性能優(yōu)化是主要指運行速度的優(yōu)化。應(yīng)用軟件對其運行速度進(jìn)行優(yōu)化是指在充分掌握軟、硬件特征的基礎(chǔ)上,通過應(yīng)用程序結(jié)構(gòu)調(diào)整等手段來縮短完成指定任務(wù)所需的運行時間,主要應(yīng)用在對實時性要求比較高的場合。
目前,移動智能系統(tǒng)的處理器在物理上也支持多線程的并發(fā)執(zhí)行,采用適當(dāng)數(shù)量的并發(fā)線程可以獲得比單一線程高的運行速度[2]。對于多核系統(tǒng)中的應(yīng)用軟件性能優(yōu)化,本文研究了基于Android系統(tǒng)的并行程序設(shè)計算法和并行程序性能優(yōu)化技術(shù)。
1 我們通過一個實際應(yīng)用來分析、研究基于移動平臺的應(yīng)用軟件優(yōu)化的技術(shù)
包括并行處理技術(shù)、多線程優(yōu)化技術(shù)以及利用GPA(Graphics Performance Analyzers)工具輔助分析技術(shù)。
本例是一個求圓周率π的應(yīng)用。有如下數(shù)學(xué)公式:
將積分公式表示為極限:
實際上Δx不可能做到無窮小,只能讓Δx盡可能小,這樣求出的結(jié)果越接近π。我們用step表示一個Δx,則num_step=1/step盡量地大??紤]到f(x)=1/(x2+1)是一個凸函數(shù),這里取一個中值來求和,即使用f[(i+0.5)/(num_steps)]來代替f[i/(num_steps)]求和,這樣求出的和不會總是比實際值偏小。最后可以得出編寫程序依據(jù)的公式為:
根據(jù)上面公式我們編寫出相應(yīng)的計算程序。
2 原始應(yīng)用(未經(jīng)優(yōu)化)研究與分析
我們首先根據(jù)上述公式直接推導(dǎo)出應(yīng)用的計算代碼,此代碼是沒有經(jīng)過優(yōu)化的,我們稱其為“原始應(yīng)用”,將其命名為SerialPi。
該應(yīng)用設(shè)計思路是:讓計算π的任務(wù)放在輔助線程(這里稱為任務(wù)線程)中運行,主活動上設(shè)置按鈕來控制線程的運行,并用一個TextView來顯示任務(wù)線程的結(jié)果。應(yīng)用運行的界面如圖1所示。
應(yīng)用啟動后的界面如圖1(a)所示。當(dāng)我們點擊“開始計算”按鈕后,界面所有的按鈕都變成灰色,直到計算π的線程運算完成。這時,界面顯示π的計算結(jié)果以及線程運行的總時間,如圖1(b)所示。
從上述運行界面可知,此應(yīng)用計算π的時間大約為22秒多。多次運行此應(yīng)用,其顯示的計算時間也大約為此時長。
圖1 SerialPi應(yīng)用運行的界面
2.1 在原始應(yīng)用 中 新 建 線 程 類MyTaskThread,讓其計算π的值。編輯源文件
MyTaskThread.java,主要代碼如下:
1 package com.example.serialpi;
2 public class MyTaskThread extends Thread {
3 private Handler mainHandler;
4 public static final int MSG_FINISHED = 1; //定義表示\"計算結(jié)束\"的消息類型
5 private static final long num_steps = 200000000;//公式中的 num_steps 變量,總步數(shù)
6 private static final double step = 1.0 / num_steps;//公式中的step變量,步長
7 public static double pi = 0.0; //π的計算結(jié)果
……
20 public void r u n ()
21 {
22 double x ,sum=0.0;
23 long i;
24 for ( i=0; i< num_steps; i++){
25 x = ( i+ 0. 5) * s t e p ;
26 sum=sum+ 4. 0 /(1.0 + x*x );
27 }
28 pI=step*sum;
29 Messagemsg=new Message();
30 msg.what=MSG _FINISHED;//定義消息類型
31 main Hand ler.send Message(msg);//發(fā)送消息}
第22行至第28行是根據(jù)計算公式書寫的計算π的代碼。這里x變量是函數(shù)f(x)=1/(x2+1)的自變量x,sum是Σ的累積變量。累積完Σ后,最后在第28行,讓π=step×∑算出最后結(jié)果?!驹诰€程的run函數(shù)中,一旦計算完成,在第29行開始,就向主線程發(fā)送計算完成的消息】
2.2 編輯主程序的源代碼文件MainActivity.java,讓其控制線程的運行,并顯示計算結(jié)果,其主要代碼如下:
1 package com.example.serialpi;
2 public class MainActivity extends Activity {
……
13 private MyTaskThread myThread = 1;
14 private TextView tv_TaskOutputInfo;// 顯示(計算)任務(wù)線程輸出
15 private Handler mHandler;;
16 private long end_time;
17 private long time;
18 private long start_time;
……
38mHandler = new Handler() {
39 public void handleMessage(Message msg) {
40 switch (msg.what)
41 {
42 case MyTaskThread.MSG_FINISHED:
43 end_time = System.currentTimeMillis();
44 time = end_time - start_time;
45 String s = \"運行結(jié)束,Pi=\"+ MyTaskThread.pi+ \" 耗時:\"
46 + MyTaskThread.msTimeToDatetime(time);
47 tv_TaskOutputInfo.setText(s);
48 btn_ExitApp.setEnabled(true);
49 break;
50 default:break; } } };
……
61 private void startTask() {
62 myThread = new MyTaskThread(mHandler); //創(chuàng)建一個線程
63if (! myThread.isAlive())
64{
65 start_time = System.currentTimeMillis();
66 myThread.start(); // 啟動線程 } }
我們首先在16行-第18行定義了三個變量,任務(wù)的開始時間start_time,任務(wù)的結(jié)束時間end_time,以及任務(wù)的運行時長time。它們存在如下關(guān)系:time=end_time-start_time。
在第65行,我們在啟動任務(wù)線程的同時,將機器的當(dāng)前時間記錄在start_time中。在第43行~第44行,當(dāng)收到任務(wù)線程運行完畢的消息后,將機器的當(dāng)前時間記錄在end_time中,兩者相減得到任務(wù)的運行時長。
2.3 使用GPA工具來驗證與評估原始應(yīng)用的性能。上述工作完成后,我們將應(yīng)用部署到實際的智能設(shè)備上進(jìn)行測試。本例我們主要監(jiān)控分析兩個CPU的負(fù)載(即“CPU XX Load”指標(biāo))情況。在監(jiān)控期間,我們點擊“開始計算”按鈕開始運行計算,記錄下GPA的監(jiān)控信息,分析的結(jié)果如圖2所示。
(a)點擊“開始計算”按鈕后
(b)線程計算運行中 (c)線程計算結(jié)束
圖2 原始應(yīng)用(SerialPi)GPA截圖
圖2(a)是點擊“開始計算”按鈕,計算(任務(wù))線程運行開始時的顯示。圖2(b)是計算(任務(wù))線程運行中的顯示。圖2(c)是計算(任務(wù))線程運行結(jié)束時的顯示。從圖中可知,計算線程沒有運行或運行結(jié)束后,CPU的負(fù)載停留在較低水平。而一旦計算線程運行,CPU的負(fù)載急劇上升到100%的滿負(fù)荷水平。但是,在計算線程運行時,兩個CPU中始終只有一個CPU是滿負(fù)荷的,另一個處于低負(fù)載水平,也就是說總的負(fù)載率(即負(fù)載之和)只有一個CPU處于滿負(fù)荷狀態(tài)。
3 對原始應(yīng)用的性能優(yōu)化
上面的例子,是從計算π的公式直接想到的代碼。這個例子有沒有可能實現(xiàn)優(yōu)化呢?怎么去優(yōu)化呢?在此,我們對算法進(jìn)行改進(jìn),充分利用多核處理器的硬件特點來進(jìn)行優(yōu)化,挖掘出多核處理器的性能潛力。
多核處理器擁有多核、多線程技術(shù),支持多線程在物理上的并行運行。例如,對于上例的運行環(huán)境,這個多核處理器就可以支持兩個線程的并行運行。這就是我們算法優(yōu)化的切入點:我們采用“分治法”的思路,設(shè)法讓計算任務(wù)分?jǐn)偟蕉鄠€(本例是兩個)線程中來運行,這樣并行運行的線程就可以加快運行速度。
首先,我們分析一下以上例子的MyTaskThread類的run函數(shù)代碼。為了計算積分面積的累計值,我們在第24行讓計算步每次累計一步來計算。實際上,我們將積分累計區(qū)域分成“塊”,每個線程負(fù)責(zé)計算其中的一塊,最后將每個線程計算的累計面積匯總起來,這樣既可以得到最后的結(jié)果,又能“分而治之”完成任務(wù)的分派執(zhí)行。這就是我們實現(xiàn)算法優(yōu)化的思路。
我們把優(yōu)化后的應(yīng)用命名為ThreadPi。該應(yīng)用在計算積分面積的累計值時,讓每個線程的計算步每次累計步長增加總線程數(shù),這樣讓每個線程負(fù)責(zé)累計自己負(fù)責(zé)的條帶的面積。該應(yīng)用運行的界面如圖3所示。
圖3 ThreadPi應(yīng)用運行的界面
此應(yīng)用運行的界面與原始應(yīng)用一致。從上述運行界面可知,此應(yīng)用計算π的時間大約為13秒多。其時間幾乎縮短到原始應(yīng)用的一半。與原始應(yīng)用不一樣的是,本應(yīng)用使用兩個線程來計算π。
3.1 本應(yīng)用是在修改原始應(yīng)用代碼的基礎(chǔ)上得到的,其關(guān)鍵的修改如下。修改計算任務(wù)線程類MyTaskThread的源代碼文件如下:
1 package com.example.threadpi;
……
4 public class MyTaskThread extends Thread{
5 private Handler mainHandler;
6 public static final int MSG_FINISHED = 1; // 定義表示\"計算結(jié)束\"的消息類型
7 private static final long num_steps = 200000000;//公式中的 num_steps 變量,總步數(shù)
8 private static final double step = 1.0 / num_steps;//公式中的step變量,步長
9 public static double pi = 0.0; //π的計算結(jié)果
10 public static final int num_threads = 2; //線程數(shù)
11private int myNum; //線程號
12private static Object sharedVariable = new Object();//對pi變量的同步鎖變量
13private static int finishedThreadNum = 0; //完成計算的線程數(shù)
……
28 public void run()
29 {
30double x, partialSum = 0.0;
31 long i;
32for (i = myNum; i < num_steps; i += num_threads) {
33 x = (i + 0.5) * step;
34 partialSum += 4.0 / (1.0 + x * x);
35 }
36 synchronized (sharedVariable) {
37 pi+=partialSum * step;
38 finishedThreadNum++;
39 if (finishedThreadNum >= num_threads){//等全部線程運行結(jié)束發(fā)送消息
40 Message msg = new Message();
41 msg.what = MSG_FINISHED;//定義消息類型
42 mainHandler.sendMessage(msg); //發(fā)送消息
43 }}}
上述代碼中陰影標(biāo)注文字是本應(yīng)用與原始應(yīng)用主要區(qū)別的地方。在第10行-第13行我們定義了多線程計算任務(wù)所需的變量。num_threads是計算任務(wù)開辟的線程數(shù)。
本例運行的設(shè)備為兩個邏輯CPU,故將此值設(shè)為2。myNum是計算線程的編號,它從0到num_threads-1范圍內(nèi)選擇。sharedVariable變量是對pi變量施加同步鎖而引入的變量。finishedThreadNum是完成計算的線程數(shù),但是該值等于num_threads就認(rèn)為所有的計算線程都已運行結(jié)束。
第30行-第43行是計算線程的原型代碼。其中第30行-第35行是計算π的直接代碼。對于原始應(yīng)用的相應(yīng)代碼可知,原始應(yīng)用的sum變量被partialSum所代替,它表示本線程計算的和(面積)只是全部和(面積)的一部分。最重要的區(qū)別是第32行,這里步長變量i累加的不是1,而是num_threads,即每一次都是向前移動線程數(shù)步。而i起始位置也不是原始應(yīng)用的從0開始,而是從線程號開始。
當(dāng)線程計算完自己的和之后,需要將此數(shù)據(jù)放到總的和(pi變量)中去。這是一個多線程共享的變量,因此需要加同步鎖。這一步驟對應(yīng)第36行~第44行。第36行加同步鎖,第37行將線程自己的計算結(jié)果加到公共結(jié)果pi中去。第38行將計算結(jié)束的線程數(shù)加一。第39行,通過比較完成計算的線程數(shù)與總的線程數(shù)來判斷全部計算線程是否都已結(jié)束,只有都結(jié)束了,才向主線程發(fā)送計算結(jié)束的消息。
3.2 修改主程序MainActivity的源代碼文件MainActivity.java如下:
1 package com.example.threadpi;
……
12 public class MainActivity extends Activity{
13 private MyTaskThread thrd[]=1;
14 private long end_time;
15private long time;
16private long start_time;
……
64 private void startTask() {
65thrd = new MyTaskThread[MyTaskThread.num_threads];
66 start_time = System.currentTimeMillis();
67 for( int i=0; i < MyTaskThread.num_threads;i++){
68 thrd[i] = new MyTaskThread(mHandler); //創(chuàng)建一個線程
69 thrd[i].setStepStartNum(i);
70 thrd[i].start();
71 } }
73 private void exitApp() {
74 for (int i = 0; i < MyTaskThread.num_threads thrd != 1; i++) {
75 try {
76 thrd[i].join(); //等待線程運行結(jié)束
77 } catch (InterruptedException e) {
78 }}
……
上述代碼中陰影標(biāo)注文字是本應(yīng)用與原始應(yīng)用主要區(qū)別的地方。在第13行,原始應(yīng)用的單個線程對象變量改為了線程數(shù)組。在啟動計算任務(wù)的第67行~第71行,原始應(yīng)用啟動單個線程,改為啟動數(shù)組中的全部線程,并在啟動時設(shè)置線程的編號。在等待線程結(jié)束的第74行-第78行,原始應(yīng)用中等待單個線程結(jié)束,改為等待線程數(shù)組中的全部線程結(jié)束。
上述工作完成后,我們同樣將應(yīng)用部署到原始應(yīng)用相同的目標(biāo)機上。我們單獨運行該應(yīng)用,來測試一下優(yōu)化后的應(yīng)用運行時間,結(jié)果是:時間幾乎縮短到原來的一半(由原來的22秒減少到13秒)。
3.3 使用GPA工具來驗證與評估優(yōu)化的應(yīng)用性能。下面使用GPA來分析此應(yīng)用,分析過程與原始應(yīng)用一致,分析的結(jié)果如圖4所示。
(a)點擊“開始計算”按鈕后
(b)線程計算運行中 (c)線程計算結(jié)束
圖4 優(yōu)化應(yīng)用(ThreadPi)的GPA截圖
從圖中可以看出,點擊“開始計算”按鈕,計算(任務(wù))線程運行開始后,兩個CPU的負(fù)載都從低負(fù)載立即上升到滿負(fù)荷狀態(tài)。計算完成后,兩個CPU的負(fù)載都重新回到低負(fù)荷狀態(tài)。與原始應(yīng)用不同是,在計算任務(wù)運行期間,全部的CPU都處于滿負(fù)荷狀態(tài),不存在負(fù)載在兩個CPU中輪流的情況。這說明該應(yīng)用的計算任務(wù)使兩個CPU并行工作在滿負(fù)荷狀態(tài),這是優(yōu)化后的應(yīng)用速度提高(本例提高將近100%)的內(nèi)在原因。
4 結(jié)束語
本文主要研究了基于移動平臺的多核系統(tǒng)的應(yīng)用軟件優(yōu)化技術(shù)。其中,多線程、平行計算技術(shù)是移動平臺系統(tǒng)的研究重點、難點,合理正確的使用這些技術(shù),可以讓多個處理器處于均衡負(fù)載狀態(tài),以提高系統(tǒng)的整體運行速度,從而達(dá)到性能優(yōu)化的目的,使我們的應(yīng)用軟件運行更加流暢、體驗更加好。
今后,我們在移動智能多核系統(tǒng)實現(xiàn)上,還有一些方面需要不斷地完善和進(jìn)一步研究的地方,比如:進(jìn)一步完善移動智能多核平臺上的并行編譯,支持更廣泛的并行模式,并且支持更廣泛的移動智能多核結(jié)構(gòu)。
參考文獻(xiàn):
[1]Butler,M.Android:Changing the Mobile Landscape.PERVASIVE COMPUTING,IEEE,2011.
[2]Ian K.T.Tan,Ian Chai,Poo Kuan Hoong.Dynamic threshold for imbalance assessment on load balancing for multicore systems[J].Computers and Electrical Engineering,2012.
[3]R.Rakvic,Q.Cai,J.González,G.Magklis,P.Chaparro,A.González.Thread-management techniques to maximize efficiency in multicore and simultaneous multithreaded microprocessors[J].ACM Transactions on Architecture and Code Optimization(TACO),2010(02).
[4]史莉雯,樊曉婭,張盛兵.單片多處理器的研究[J].計算機應(yīng)用研究,2007(09):46-49.
[5]李濤,高德遠(yuǎn),樊曉婭.高性能微處理器性能模型設(shè)計[J].航空電子技術(shù)2011(02):25-28.
[6]ShahidBokhari,JoelSaltz.Exploring the performance of massively multithreaded architectures[J].Concurrency Computat:Pract.Exper,2010(05).
[7]Stijn Eyerman,Lieven Eeckhout.Modeling critical sections in Amdahl’s law and its implications for multicore design[J].ACM SIGARCH Computer Architecture News,2010(03).
作者簡介:何偉文(1970-),男,廣東廣州人,碩士,高級工程師,研究方向:網(wǎng)絡(luò)安全,系統(tǒng)工程,項目管理。
作者單位:廣州涉外經(jīng)濟職業(yè)技術(shù)學(xué)院 信息學(xué)院,廣州 510540