OpenCL 性能分析事件、工作項同步化

1.性能事件分析
1.1配置性能分析命令
爲了獲得一條命令的時序信息,需要分別做以下三步:

在用clCreateCommandQueue函數創建命令隊列時,設置CL_QUEUE_PROFILING_ENABLE標誌;
將cl_event所要進行性能分析的命令關聯起來。如前所述,這可以將事件的指向設爲入列命令的函數的最後一個參數。
在執行完後,調用clGetEventProfilingInfo函數來訪問cl_event,獲取命令的時序信息。

第一步是使能對命令隊列的性能分析,讓OpenCL記錄隊列中命令改變狀態的時間。第二部是指定cl_event對象,保存特定命令的時序信息,最後一步是從cl_event對象中獲取數據。clGetEventProfilingInfo函數的簽名:

cl_int clGetEventProfilingInfo(cl_event event,cl_profiling_info param,size_t param_value_size,void*param_value,size_t *param_value_size_ret)

每一種情況下,clGetEventProfilingInfo函數所返回的都是64位數,表示命令狀態改變的時間,單位爲ns。爲了確定命令在隊列中的時間,先後設置CL_PROFILING_COMMAND_SUBMIT標識和CL_PROFILING_COMMAND_QUEUED標識,兩次調用clGetEventProfilingInfo函數,然後計算時間差即可。類似的,如果想知道命令執行所用的時間,先後設置CL_PROFILING_COMMAND_START和CL_PROFILING_COMMAND_END,兩次調用clGetEventProfilingInfo函數,計算時間差即可。
性能分析的時間單位爲ns。但並不是所有設備的時間分辨率都可以達到幾個ns。所以爲了獲取設備關於時間分辨率的信息,可以將CL_DEVICE_PROFILING_TIMER_RESOLUTION設爲第二個參數,調用clGetDevice函數即可。下面的代碼所示的就是整個操作過程:

size_t time_res;
clGetDeviceInfo(device,CL_DEVICE_PROFILING_TIMER_RESOLUTION,
sizeof(time_res),&time_res,NULL);

代碼行返回size_t型數據,對應的是定時器計數比那花的耗時效果(單位爲ns)。
性能分析命令的工作程序先是創建一個使能時序分析的命令隊列,然後將讀取緩存對象的命令入列,再調用clGetEventProfilingInfo函數來訪問得到命令的時序信息。性能分析來確定讀寫操作和內存映射這兩種方式,哪種方式傳輸數據更快。
1.2對數據傳輸進行性能分析
主機和設備之間有兩種傳輸數據的方式。可以調用函數(例如clEnqueueReadBuffer或clEnqueueWriteImage)來入列讀寫命令,也可以用clENqueueMapBUffer這樣的函數來將內存對象映射到主機內存中,在處理完映射數據之後,調用clEnqueueUnmapMemObject函數來解映射內存區域。對於文件訪問而言,內存映射這種方法的運行性能更高。
對被測試的不同大小的數據,內存映射傳輸的方式都快於讀命令的方式。隨着傳輸數據大小的增大,性能也變得更加明顯。
需要指出的是,在計算內存映射傳輸的耗時時,只考慮了clEnqueueMapBuffer函數,並沒有對clEnqueueUnmapMemObject函數進行性能分析。當然,數據傳輸的時間也遠遠小於命令執行的時間。
1.3對數據劃分進行分析
數據劃分可以將內核的執行分配到多個工作項上。理論上講,執行內核所需的時間會隨着工作項的增多而減少。對clEnqueueNDRangeKernel函數進行性能分析。和clEnqueueTask函數一樣,這個函數所入列的是內核執行命令,但它還有些參數,表示的是內核執行的劃分方式。這個函數比較複雜:

clEnqueueNDRangeKernel(cl_command_queue queue,cl_kernel kernel,cl_uint work_dims,const size_t *global_work_offset,const size_t *global_work_size,const size_t *local_work_size,cl_uint num_events,const cl_event *wait_list,cl_event *event)

第三個參數work_dims指明瞭數據的維度。第五個參數global_work_size指明瞭每個維度對所對應生成的工作項的數量。最後一個參數event是一個cl_event對象,用來監視內核執行命令。
clEnqueueReadBuffer函數和clEnqueueMapBuffer函數被設爲阻塞型,而clEnqueueNDRangeKernel是非阻塞性函數。因此函數會在內核執行完成前返回。這是一個問題,因爲後面的代碼測量的是內核執行的起止時間。我們也可以用回調函數來獲取函數執行的時序信息,但因爲使用的是clFinish函數,所以應用程序會在內核執行完之後才返回。
將內核劃分爲多個工作項的重要性。開始是從一個工作項到兩個和四個工作項,內核執行的耗時基本上是隨着工作項的倍增而遞減。但隨着工作項數量的逐漸增加,曲線的下降幅度就變得不是很明顯。
主機應用程序向內核發送了兩個參數,第一個是緩存對象,其中包含了待處理的整型數據。第二個參數指明瞭緩存對象中整型數據的數量。每個工作項都會用這第二個參數(num_ints)和全局工作項數量來確定有多少個向量需要處理,在profile_items.cl原程序中對應的代碼行如下:

int num_vectors = num_ints/(4 *get_global_size(0));

這行代碼保證了各個工作項訪問的數據是不同的。但如果你需要工作項訪問共同的數據一起工作,就需要用到同步化操作。
2.工作項同步化
同步化的定義爲:一個保證運算任務有序執行的過程。OpenCL提供了兩類同步化:命令同步化和工作項同步化。命令同步化就是通過使用事件或clEnqueueBarrier這樣的函數來調整命令間的執行順序。目前爲止,我們的程序都不需要工作項間的同步化。這些共走向都是訪問不同的內存空間,所以互相之間的數據訪問也就互不影響。但是如果多個工作項訪問的是保存數據的同一段空間,工作項之間的執行順序就變得非常重要。例如,對兩個向量進行點成運算,每個工作項就必須能訪問和修改最後的乘積之和。
只有命令隊列屬於同一個上下文,這樣的命令纔可以同步化。同樣,只有工作項屬於同一個工作組,纔可以實現工作項同步化。
使用clEnqueueNDRangeKernel函數來配置工作組。
回顧三種主要類型的地址空間:

和處理單元相關的內存被稱爲私有內存(private memory)
和計算單元相關的內存被稱爲局部內存(local memory)
設備上都可以訪問的內存被稱爲全局內存(global memory)

處理單元對私有內存的訪問是設備上最快的內存訪問方式。相對而言,局部內存訪問就比較慢,全局內存訪問則更慢。所以,對於高性能數據處理,要儘可能多的使用私有內存和局部內存。
如果多個工作項訪問的時局部內存中的同一段數據,他們之間的執行順序就可能得到不同的結果,甚至是錯誤,爲了防止這些錯誤的發生,就需要對工作項之間的執行進行同步化,OpenCL提供了兩種方法。第一種是使用了柵欄(fence)和障礙(barrier),第二種是使用了原子操作。
2.1障礙和柵欄
clEnqueueBarrier函數就是用來幫助實現命令之間的同步化。障礙命令會延遲後面命令的執行時間,直到所有當前命令執行完成。
爲了對同一個工作組進行同步化,OpenCL提供了barrier函數。這個函數可以迫使一個工作項延遲執行時間,等到同組中的其他工作項都達到障礙之後,纔開始執行。函數簽名:

void barrier(cl_fence_flags flags)

通過創建一個障礙,就可以確保每個工作項都同時到達處理過程中的某個時間點。這才需要用工作項運算得到中間值,來進行後面的運算時非常有用。
例如假設你需要用一個工作組來計算一個大型、複雜物體的衝量。第一步是確定物體的體積,第二步是將這個體積乘以物體的密度和速度得到物體的動量。內核的實現代碼爲:

compute_volume();
compute_momentum();

如果在其他的工作項計算出體積之前,就有工作項開始處理compute_momentum函數,得到的結果就會因爲體積計算的不準確而不準確。但我們可以通過在兩個任務之間加入障礙函數來解決這個問題:

compute_volume();
barrier(CL_LOCAL_MEM_FENCE);
compute_momentum();

CL_LOCAL_MEM_FENCE標誌確定了內存操作是如何影響和工作組局部內存有關的操作的。例如,每個工作組都必須在障礙函數同步化之前,處理完對局部內訓的訪問。同樣,如果當標誌位設置爲CL_GLOBAL_MEM_FENCE,障礙函數會同步化對全局內存的訪問。
barrier函數只會同步化一個工作組內的工作項。除了設計新的內核,不然沒有辦法同步化不同工作組內的工作項。
柵欄和障礙很相似,但它主要用來同步化特定的內存操作,有些柵欄負責同步化讀操作,而有些則用來同步化寫操作。OpenCL提供了三種柵欄函數用來同步化內核的內存訪問:

void read_mem_fence(cl_mem_fence_flags flags)--暫停讀內存操作,直到之前的讀內存操作全部完成;
void write_mem_fence(cl_mem_fence_flags flags)--暫停寫內存操作,直到之前的寫內存操作全部完成;
void mem_fence(cl_mem_fence_flags flags)--暫停讀寫內存操作,直到之前的讀寫內存操作全部完成;

這些函數參數列表中的flags參數與barrier一樣,可以接受兩種取值:CLK_LOCAL_MEM_FENCE用來同步化局部內存訪問,或者CLK_GLOBAL_MEM_FENCE用來同步化全局內存訪問。
2.2原子操作
考慮下面一行代碼:

x -= 4;

這一個操作包含以下三步子操作:讀取x的取值,然後減4,並保存計算結果。如果工作項訪問的時不同的內存區域,這三步子操作就必須要有序完成。

u/int atomic inc(volatile__(g|l) u/int*x)--對存在x的值自增(*x +=1)
u/int atomic xchg(volatile__(g|l)u/int*x,u/int val)--將x所存值與32位val交換(*x=val)

這其中的每一個函數都會對全局內存或局部內存中的標量進行更新,然後返回*x原來的取值。除了atomic xchg函數之外,這些操作都是對int型或unsigned int型量進行原子操作,他們必須同時是int型或unsigned型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章