• 沒有找到結果。

適合異質多核心平台的應用程式移植步驟

3. 系統架構與實作

3.3. 適合異質多核心平台的應用程式移植步驟

一般來說要把一個普通單核心的應用程式移植到異質多核心的平台上,首先 要把原本的程式進行分割,看哪部分工作適合這個核心執行,哪部分工作適合另 一個核心執行,但除了這種為了平行化的效能而修改演算法的工作分配之外,底 層還是需要有為了多個核心上資料的溝通,使資料共用,而對原本的程式碼做些 微修改,在這方面提出一種porting 樣式,這種 porting style 主要目的是為了簡化 這種修改,使得每次單核心版本的程式碼有更新時,要再 porting 到多核心時,

可減少不必要的額外修改。

主要分為三個步驟,(1)變更變數的資料型態,(2)初始化時變數在記憶體上 的存放位址,(3)在資料需要同步時對這類變數去做搬移。

3.3.1. 變更資料型態

一般常見的CPU 都是以 byte (8bit)為記憶體 addressing 單位,而有些 DSP 依 設計的不同可能會有以word (16bit)為記憶體 addressing 單位,為了適應不同核心 的addressing 單位,多核心要共用資料就要以 addressing 單位最大的為基準。以 下的例子就分別以CPU 用 8bit 而 DSP 用 16bit 為 addressing 單位來說明,所以 CPU 和 DSP 有共用資料的話,其 addressing 單位以 16bit 為基準。假如 CPU 和 DSP 現在要共用一個 8bit 陣列,其原本的型態假設是 uint8,這時就另外定義 share_uint8 為 uint16,並把原本的型態改成 share_uint8,這樣做的優點是,可以 很明確的知道這個陣列是要來共用的,也知道原本的型態是 uint8,而實際要存 取任一筆資料時,不管是用陣列或是指標的方式,compiler 就依照其實際的型態 uint16 來計算位址,會把 offset 的單位從 1(byte)產生成 2(halfword),所以對程式 碼來說,就只有型態有關的部分需要修改,如函數的參數傳遞、資料轉型和sizeof 函數等,存取的部分不需要在修改;唯一的缺點是記憶體addressing 單位小的核

uint8 可表示範圍內的值就不會有錯誤。

3.3.2. 初始化記憶體位址

一般的程式寫作大都不會強制設定變數存放的記憶體位址,而是用 malloc 這類的函式來幫我們分配其位置,但在這種異質多核心的硬體平台設計,大多會 設計讓各個核心都能存取的記憶體區塊來做為共用記憶體,如 SRAM 記憶體,

甚至有些核心會有自己專用的scratchpad 記憶體,這類記憶體的特點就是存取速 度比一般的SDRAM 記憶體還快,所以資料如果能存放在上面運算的存取速度也 就能提升。因此就有需要手動設定共用或是常存取的變數在記憶體上存放的位 址。我們可以新增一個 shared_mem.c 程式檔給多個核心共用,這樣的優點是對 各個核心的記憶體位址和大小的配置可以很容易同步,而為了適應各個核心上的 差異、如共用記憶體區塊的起始位址和 addressing 單位,可以用#if 這種方式來 判別現在是要compile 給哪個核心,然後各自定義應給的值;原本用 malloc 函式 得來的存放的位址,就改成剛設定的記憶體位址,以Figure 37 的程式碼來當例 子。假如在共用記憶體上有兩個128 byte 的陣列,在之前改變資料型態裡面所提 到的,要以addressing 單位比較大的為基準,所以陣列的每個 8bit 是儲存在 DSP 的addressing 單位的 16bit(2byte)上,所以每個陣列實際會佔用 256 byte,而 DSP 因為其addressing 單位 16bit 是 ARM addressing 單位 8bit 的兩倍,所以要把記憶 體位址再除以MEM_UINT(2),才是 128 個 16bit。

share_mem.c

#ifdef DSP

#define SHARE_MEM 0x600000

#define MEM_UNIT 2

#else

#define SHARE_MEM 0x20000000

#define MEM_UNIT 1

#endif

share_uint8 *array1_ptr = (share_uint8 *)(SHARE_MEM+0x00000)/MEM_UNIT);

share_uint8 *array2_ptr = (share_uint8 *)(SHARE_MEM+0x00100)/MEM_UNIT);

init.c

#include <share_mem.h>

void init()

{ array1 = (share_uint8 *) array1_ptr;//(uint8 *)malloc(128*sizeof(uint8));

array2 = (share_uint8 *) array2_ptr;//(uint8 *)malloc(128*sizeof(uint8)); }

Figure 37. Shared Memory Initializatin Example

3.3.3. 資料搬移

基本上把資料放在共用記憶體上運算,各個核心存取到的就已經當前的值,

除非是因為有核心使用了Data cache 就有可能造成 coherency 的問題,所以在需 要同步前要能 flush 掉 cache,或是事先要設定共用記憶體的這段區塊是不會被 cache 的。而之前有提到過異質多核心平台可能有會 scratchpad 記憶體的設計,

所以如果共用的資料是經常會存取的話,就在 scratchpad 記憶體多分配一份空 間,然後在同步完後搬進scratchpad 記憶體,對其運算時的存取將會加快,缺點 就是要多一次搬移的 overhead。當這類資料量太大,可以改用直接記憶體存取 (DMA)的方式,直接搬移兩個核心的記憶體,而不經由共用記憶體,可加速搬移 的時間。

正如之前在變更資料型態裡有提到的多核心共用資料要以記憶體 addressing 單位最大的為基準,如果要運算的資料單位小於最大的記憶體addressing 單位,

每 單 位 資 料 存 放 在 共 用 記 憶 體 要 用 到 的 空 間 大 小 , 也 就 是 最 大 的 記 憶 體 addressing 單位。因此資料搬移的方式,就不能使用目標和來源單位大小一致的 memcpy 函數。

for(int i=0; i<size; i++)

*((share_uint8*)dst + i) = src[i];

memcpy(dst, src, size);

Figure 38. Memcpy to Looply Copy

如Figure 38,有一個 uint8 的陣列要搬到共用記憶體上,原本的目標會和來 源一樣都是 uint8*,但最大的 addressing 單位是 16bit,因此目標就要改成 share_uint8,也就是 uint16,因此目標和來源的大小就不一致,memcpy 就要改 成用for 迴圈的方式,搬移就變比較沒有效率,但這樣搬移的結果才會是正確的;

當然最後運算完,資料整合也需要類似的方式來存取。

所以如果compiler 能針對這類的 pattern,產生出效率跟 memcpy 函式接近的 指令,或是DMA 能夠支援來源和目標的資料單位大小可以不一致,將可以減少 不同核心間資料搬移的overhead。

相關文件