本文講述如何在Mali-T600系列GPU和CPU之間高效共享內存。
介紹
當處理大量數據時(在OpenCL應用中這是典型的情況),確保主機與OpenCL設備之間儘可能高效地共享內存是非常重要的。我們已在hello
world樣例中看到了如何使用內存緩衝區。hello world例程遵循了我們認爲的對於在主機和OpenCL設備之間共享內存的“最佳實踐”。這一教程講述這些最佳實踐方法。除非另作說明,否則所有代碼片段來自"hello_world_vector.cpp"。
共享內存系統
典型地,OpenCL運行在應用處理器和GPU具有獨立內存的系統上。爲了在這些系統中共享CPU與GPU之間的內存,你必須分配緩衝區,拷貝數據從/來自獨立內存。
典型地,帶Mali GPU的系統具有共享內存,因此你無需拷貝數據。然而,OpenCL假設內存是分開的,緩衝區分配涉及內存拷貝。這相當浪費,因爲拷貝需要時間並且消耗能量。
跨設備數據共享
爲了避免拷貝,使用OpenCL API來分配內存緩衝區,並使用映射操作。這些操作同時使能了應用處理器和Mali GPU來訪問數據而無需任何拷貝。注意:在分配內存緩衝區之前,必須先初始化OpenCL。
1. 分配內存緩衝區
應用程序通過調用clCreateBuffer(),來創建可以傳遞數據從/到內核的緩衝區對象。所有的內存都分配在一個共享的內存系統上,該共享的內存系統在物理上可以同時被CPU和GPU核訪問。然而,只有由clCreateBuffer()分配的內存,纔會被同時映射到CPU和GPU的虛擬內存空間。因此,由malloc()等函數分配的內存,只會被映射到CPU上(如圖1)。
圖1:由用戶(malloc)分配的內存不會被映射到GPU內存空間
因此,使用CL_MEM_USE_HOST_PTR標誌調用clCreateBuffer(),傳遞到用戶創建的緩衝區,需要創建一個新的緩衝區並拷貝數據(同CL_MEM_COPY_HOST_PTR)。這個拷貝使得性能下降(如圖2)。
圖2:clCreateBuffer(CL_MEM_USE_HOST_PTR)創建一個新的緩衝區,然後複製數據(拷貝操作是代價高昂的)
所以,總是儘可能地使用CL_MEM_ALLOC_HOST_PTR標誌。這樣就分配了一塊CPU和GPU都可以訪問的內存,無需拷貝(如圖3)。
圖3:clCreateBuffer(CL_MEM_ALLOC_HOST_PTR)創建了一個同時對CPU和GPU可見的緩衝區
memoryObjects[0] = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_ALLOC_HOST_PTR, bufferSize, NULL, &errorNumber);
createMemoryObjectsSuccess &= checkSuccess(errorNumber);
memoryObjects[1] = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_ALLOC_HOST_PTR, bufferSize, NULL, &errorNumber);
createMemoryObjectsSuccess &= checkSuccess(errorNumber);
memoryObjects[2] = clCreateBuffer(context, CL_MEM_WRITE_ONLY | CL_MEM_ALLOC_HOST_PTR, bufferSize, NULL, &errorNumber);
createMemoryObjectsSuccess &= checkSuccess(errorNumber);
2. 映射內存對象
如前面所述,我們映射由OpenCL實現創建的內存緩衝區到指針,從而可以在CPU端訪問它們。
cl_int* inputA = (cl_int*)clEnqueueMapBuffer(commandQueue, memoryObjects[0], CL_TRUE, CL_MAP_WRITE, 0, bufferSize, 0, NULL, NULL, &errorNumber);
mapMemoryObjectsSuccess &= checkSuccess(errorNumber);
cl_int* inputB = (cl_int*)clEnqueueMapBuffer(commandQueue, memoryObjects[1], CL_TRUE, CL_MAP_WRITE, 0, bufferSize, 0, NULL, NULL, &errorNumber);
mapMemoryObjectsSuccess &= checkSuccess(errorNumber);
3. 使用C指針初始化數據
一旦緩衝區被映射到一個cl_int類型的指針,它們可以像普通的C指針那樣來使用,以初始化數據。
for (int i = 0; i < arraySize; i++)
{
inputA[i] = i;
inputB[i] = i;
}
4. 取消內存對象的映射
當我們結束使用內存對象,從CPU端對它們取消映射。之所以這麼做是因爲:
(1).
在OpenCL端的內核內部,讀寫那個內存的行爲是未定義的;
(2).
在完成時,OpenCL實現無法釋放那個內存。
if (!checkSuccess(clEnqueueUnmapMemObject(commandQueue, memoryObjects[0], inputA, 0, NULL, NULL)))
{
cleanUpOpenCL(context, commandQueue, program, kernel, memoryObjects, numberOfMemoryObjects);
cerr << "Unmapping memory objects failed " << __FILE__ << ":"<< __LINE__ << endl;
return 1;
}
if (!checkSuccess(clEnqueueUnmapMemObject(commandQueue, memoryObjects[1], inputB, 0, NULL, NULL)))
{
cleanUpOpenCL(context, commandQueue, program, kernel, memoryObjects, numberOfMemoryObjects);
cerr << "Unmapping memory objects failed " << __FILE__ << ":"<< __LINE__ << endl;
return 1;
}
補充建議
除了上面的建議,我們附加下述建議:
(1).請勿使用私有(private)或局部(local)內存,使用這些在Mali-T600系列GPU上並無性能增益。
(2). 如果你的內核是在有限內存帶寬的情況下,使用一個公式來計算變量,而不是從內存中讀取。