# 2017q1Homewok1(raytracing) contributed by <`janetwei`> ### Reviewed by `ierosodin` - OpenMP 與 pthread 的效能差異不大 - 比較不同 thread number 對效能的差異 - 利用 threadpool 來解決不同 thread 之間工作量不平均的問題 - OpenMP 中 不同 schedule 對平行化的影響 - 用 SIMD 來加速整個資料的處理 ## 開發環境 `$ lscpu ` Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 2 Core(s) per socket: 2 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 61 Model name: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz Stepping: 4 CPU MHz: 2220.750 CPU max MHz: 2700.0000 CPU min MHz: 500.0000 BogoMIPS: 3199.77 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 3072K NUMA node0 CPU(s): 0-3 **優化前原始程式的執行時間:** `Execution time of raytracing() : 3.001049 sec` base.png ![](https://i.imgur.com/Z4QMg03.png) ## gprof **gprof:** 印出程式執行中每個 function 所花的時間,可以幫助從眾多 function 中找出耗時最多的。其中包括呼叫次數,因此可以幫忙分析程式運行流程 `$ make PROFILE=1` `$ ./raytracing` `Execution time of raytracing() : 6.391525 sec` 在編譯和鏈接程序時(加入`-pg`),gcc 在你的每个函数中都加入了mcount, 依赖於 compiler 或操作系统的函數,因此 gprof 所執行的時間較慢 **gprof2dot** 能將 gprof 的結果轉成一個結點格式,分析各個點(也就是 function )之間的關係 `$ git clone https://github.com/jrfonseca/gprof2dot ` `$ gprof raytracing| gprof2dot/gprof2dot.py | dot -Tpng -o result.png ` ![](https://i.imgur.com/jVcM23I.png) ### 開始分析 `$ gprof -b raytracing gmon.out | less` 這裡用`less`是因為 gprof 輸出訊息較多,`less`可以讓我們透過上下方向鍵查看 gprof 產生的輸出 ``` Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 24.47 0.56 0.56 69646433 0.00 0.00 dot_product 23.60 1.10 0.54 56956357 0.00 0.00 subtract_vector 9.61 1.32 0.22 31410180 0.00 0.00 multiply_vector 8.74 1.52 0.20 4620625 0.00 0.00 ray_hit_object 7.65 1.70 0.18 13861875 0.00 0.00 rayRectangularIntersection 5.46 1.82 0.13 13861875 0.00 0.00 raySphereIntersection 5.02 1.94 0.12 17836094 0.00 0.00 add_vector 4.81 2.05 0.11 10598450 0.00 0.00 normalize 3.93 2.14 0.09 1048576 0.00 0.00 ray_color 1.75 2.18 0.04 4221152 0.00 0.00 multiply_vectors 1.31 2.21 0.03 17821809 0.00 0.00 cross_product 0.66 2.22 0.02 3838091 0.00 0.00 length 0.44 2.23 0.01 2110576 0.00 0.00 compute_specular_diffuse 0.44 2.24 0.01 2110576 0.00 0.00 localColor 0.44 2.25 0.01 1241598 0.00 0.00 protect_color_overflow 0.44 2.26 0.01 1241598 0.00 0.00 reflection 0.44 2.27 0.01 1241598 0.00 0.00 refraction 0.44 2.28 0.01 1048576 0.00 0.00 rayConstruction 0.44 2.29 0.01 1 0.01 2.29 raytracing 0.00 2.29 0.00 2558386 0.00 0.00 idx_stack_empty 0.00 2.29 0.00 2520791 0.00 0.00 idx_stack_top 0.00 2.29 0.00 1204003 0.00 0.00 idx_stack_push 0.00 2.29 0.00 1048576 0.00 0.00 idx_stack_init 0.00 2.29 0.00 113297 0.00 0.00 fresnel 0.00 2.29 0.00 37595 0.00 0.00 idx_stack_pop 0.00 2.29 0.00 3 0.00 0.00 append_rectangular 0.00 2.29 0.00 3 0.00 0.00 append_sphere 0.00 2.29 0.00 2 0.00 0.00 append_light 0.00 2.29 0.00 1 0.00 0.00 calculateBasisVectors 0.00 2.29 0.00 1 0.00 0.00 delete_light_list 0.00 2.29 0.00 1 0.00 0.00 delete_rectangular_list 0.00 2.29 0.00 1 0.00 0.00 delete_sphere_list 0.00 2.29 0.00 1 0.00 0.00 diff_in_second 0.00 2.29 0.00 1 0.00 0.00 write_to_ppm ``` 由 gprof 結果顯示 dot_product & subtract_vector 這兩個 function 的呼叫次數最多,執行時間分別佔用總時間的24.47% 和 23.6% ,因此嘗試優化這兩個 function ### compiler優化(-Ofast) `Execution time of raytracing() : 0.594889 sec` ## 優化 ### loop unrolling `math-toolkit.h`的 `dot_product()` & `subtract_vector()` 兩個 functions 的是由簡單的 for 迴圈組成,因此嘗試將迴圈拆開 ```C= void subtract_vector(const double *a, const double *b, double *out) { out[0] = a[0] - b[0]; out[1] = a[1] - b[1]; out[2] = a[2] - b[2]; } ``` ```C= double dot_product(const double *v1, const double *v2) { double dp = 0.0; dp = v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]; return dp; } ``` 執行時間: `Execution time of raytracing() : 1.996303 sec` 再由 gprof 分析 ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 17.75 0.28 0.28 69646433 0.00 0.00 dot_product 11.62 0.46 0.18 56956357 0.00 0.00 subtract_vector 11.30 0.63 0.18 17836094 0.00 0.00 add_vector 9.68 0.78 0.15 13861875 0.00 0.00 rayRectangularIntersection 9.36 0.93 0.15 17821809 0.00 0.00 cross_product 8.07 1.05 0.13 13861875 0.00 0.00 raySphereIntersection 7.75 1.17 0.12 31410180 0.00 0.00 multiply_vector 6.46 1.27 0.10 10598450 0.00 0.00 normalize 5.81 1.36 0.09 4620625 0.00 0.00 ray_hit_object 3.23 1.41 0.05 1048576 0.00 0.00 ray_color 1.94 1.44 0.03 1241598 0.00 0.00 reflection 1.29 1.46 0.02 2110576 0.00 0.00 localColor 1.29 1.48 0.02 1241598 0.00 0.00 protect_color_overflow 1.29 1.50 0.02 1241598 0.00 0.00 refraction ``` 發現 `dot_product()` & `subtract_vector()` 仍為最高的呼叫次數,但是已經不到 20% ,`subtract_vector()`甚至已經減少到 11.62% 因此將其他也是由迴圈所構成的函數 loop unrolling 執行時間: `Execution time of raytracing() : 2.079975 sec` ![](https://i.imgur.com/L7NYjZ9.png) ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 15.80 0.21 0.21 69646433 0.00 0.00 dot_product 12.41 0.38 0.17 13861875 0.00 0.00 raySphereIntersection 12.41 0.54 0.17 17821809 0.00 0.00 cross_product 10.91 0.69 0.15 56956357 0.00 0.00 subtract_vector 9.40 0.81 0.13 13861875 0.00 0.00 rayRectangularIntersection 7.52 0.91 0.10 4620625 0.00 0.00 ray_hit_object 6.39 1.00 0.09 31410180 0.00 0.00 multiply_vector 6.02 1.08 0.08 17836094 0.00 0.00 add_vector 3.39 1.12 0.05 4221152 0.00 0.00 multiply_vectors 3.01 1.16 0.04 10598450 0.00 0.00 normalize 2.26 1.19 0.03 1048576 0.00 0.00 ray_color 2.26 1.22 0.03 1 0.03 1.33 raytracing 1.50 1.24 0.02 3838091 0.00 0.00 length 1.50 1.26 0.02 2520791 0.00 0.00 idx_stack_top ``` **gprof2dot** ![](https://i.imgur.com/IhdaLIZ.png) ### force inline 在呼叫函式時會需要分配記憶空間因而需要額外的資源負擔,可以「建議」編譯器將之設定為「行內函式」(Inline function),如果建議被採納,則該函式會自動在呼叫點展現為程式碼,行內函式建議可以直接定義於表頭檔案中 將`math-toolkit.h`中的 `inline` 替換成` __attribute__((always_inline)) ` ```C= static __attribute__((always_inline)) void normalize(double *v) { double d = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); assert(d != 0.0 && "Error calculating normal"); v[0] /= d; v[1] /= d; v[2] /= d; } ``` 執行時間: `Execution time of raytracing() : 1.737352` sec ![](https://i.imgur.com/1tHLSBx.png) `$ make PROFILE=1`後會出現`always_inline function might not be inlinable`的 warning =>指的是compiler可能不會做inline這個指令 ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 34.01 0.60 0.60 13861875 0.00 0.00 rayRectangularIntersection 24.29 1.02 0.43 13861875 0.00 0.00 raySphereIntersection 12.29 1.24 0.22 2110576 0.00 0.00 compute_specular_diffuse 7.72 1.37 0.14 4620625 0.00 0.00 ray_hit_object 7.43 1.50 0.13 1048576 0.00 0.00 ray_color 6.86 1.62 0.12 2110576 0.00 0.00 localColor 2.57 1.67 0.05 1048576 0.00 0.00 rayConstruction 1.71 1.70 0.03 1 0.03 1.75 raytracing 1.14 1.72 0.02 1241598 0.00 0.00 reflection 0.57 1.73 0.01 2558386 0.00 0.00 idx_stack_empty ``` 發現`rayRectangularIntersection` 和 `raySphereIntersection` 的呼叫次數最多 ![](https://i.imgur.com/bCtcIJk.png) ### OpenMP OpenMP 是一套支援跨平台共享記憶體方式的多執行緒並行的編程API,使用C,C++和Fortran語言,包括一套編譯器指令、庫和一些能夠影響執行行為的環境變數。 [參考資料](http://mropengate.blogspot.tw/2016/06/openmp.html) 在 `Makefile` 中加入參數 `-fopenmp` ```C= CC ?= gcc CFLAGS = \ -std=gnu99 -Wall -O0 -g -fopenmp LDFLAGS = \ -lm -fopenmp ``` 由 gprof2dot 顯示出`rayRectangularIntersection` 和 `raySphereIntersection`都是由 `ray_hit_object` 中呼叫的,因此嘗試在此 function 平行化 for ```C= #pragma omp parallel for for (rectangular_node rec = rectangulars; rec; rec = rec->next) { if (rayRectangularIntersection(biased_e, d, &(rec->element), &tmpresult, &t1) && (t1 < nearest)) { /* hit is closest so far */ *hit_rectangular = rec; nearest = t1; result = tmpresult; } } /* check the spheres */ for (sphere_node sphere = spheres; sphere; sphere = sphere->next) { if (raySphereIntersection(biased_e, d, &(sphere->element), &tmpresult, &t1) && (t1 < nearest)) { *hit_sphere = sphere; *hit_rectangular = NULL; nearest = t1; result = tmpresult; } } ``` 失敗!!!! 出現 error,因為 OpenMP 的 for 有一定的格式 [OpenMP Application Program Interface](http://www.openmp.org/wp-content/uploads/OpenMP3.1.pdf) **for (init-expr; test-expr; incr-expr)** * **init-expr:** var = lb integer-type var = lb random-access-iterator-type var = lb pointer-type var = lb * **test-expr:** var relational-op b b relational-op var * **incr-expr:** ++ var var ++ --var var-- var += incr var -= incr var = var + incr var = incr + var var = var - incr 之後嘗試在 `raytracing` 用 OpenMP ``` #pragma omp parallel private(object_color,stk,d) #pragma omp for schedule(static,omp_get_max_threads()) ``` [OpenMP 語法說明](https://kheresy.wordpress.com/2006/09/13/%e7%b0%a1%e6%98%93%e7%9a%84%e7%a8%8b%e5%bc%8f%e5%b9%b3%e8%a1%8c%e5%8c%96%ef%bc%8dopenmp%ef%bc%88%e4%ba%8c%ef%bc%89%e8%aa%9e%e6%b3%95%e8%aa%aa%e6%98%8e/) **omp_get_max_threads:** Returns an integer that is equal to or greater than the number of threads that would be available if a parallel region without num_threads were defined at that point in the code. **private:** 讓每個執行緒中,都有一份變數的複本,以免互相干擾。 **schedule:** 設定 for 迴圈的平行化方法;有 dynamic、guided、runtime、static 四種方法。 執行時間: `Execution time of raytracing() : 0.823155 sec` ![](https://i.imgur.com/zedEMZI.png) 用 OpenMP 使效能有明顯的提升 ![](https://i.imgur.com/OmCmbRd.png) ### pthread pthread: 可將一個 function 分成多個 thread 執行,增加執行效率 #### main.c 加入標頭檔 `#include <pthread.h>` 宣告 thread id ,並配置記憶體空間給他 並且宣告一個指標陣列,使得每個 id 都有值傳入( tmp[i] ) ```C= pthread_t *id = (pthread_t *) malloc(THREAD_NUM * sizeof(pthread_t)); rays_arg **tmp = (rays_arg **) malloc(THREAD_NUM * sizeof(rays_arg)); for(int i=0; i<THREAD_NUM; i++) { tmp[i]=(rays_arg *)malloc(sizeof(rays_arg)); tmp[i]->pixels = pixels; tmp[i]->background_color = background; tmp[i]->rectangulars = rectangulars; tmp[i]->spheres = spheres; tmp[i]->lights = lights; tmp[i]->view = &view; tmp[i]->width = ROWS; tmp[i]->height = COLS; tmp[i]->t_id = i; tmp[i]->t_num = THREAD_NUM; pthread_create(&id[i], NULL, (void *) &raytracing, (void *) tmp[i]); } for(int i=0; i<THREAD_NUM; i++) { pthread_join(id[i],NULL); } ``` * **pthread_create():** 產生一個 thread 執行 function `int pthread_create(pthread_t *tid , const pthread_attr_t *attr , void *(*function)(void *) , void *argument)` const pthread_attr_t *attr為該Thread的屬性,預設是NULL, void *argument為Function pointer所要帶的參數。 回傳值: 如果執行成功則回傳0,如果執行失敗則回傳錯誤。 * **pthread_join():** 等待指定的 thread 結束 `int pthread_join (pthread_t thread, void **value_ptr)` #### raytracing.c * **pthread_exit():** 用來關閉一個 thread `void pthread_exit (void *value_ptr)` #### raytracing.h 由於 `main.c`中的 pthread_create() 只能輸入一個變數,因此創一個 struct 使得那個變數指向 raytracing()所需的變數 ```C= typedef struct ray { uint8_t *pixels; double * background_color; rectangular_node rectangulars; sphere_node spheres; light_node lights; const viewpoint *view; int width; int height; int t_id; int t_num; } rays_arg; ``` `Execution time of raytracing() : 0.798060 sec` ![](https://i.imgur.com/TUVts2E.png) 可是優化的結果只有和OpenMP相差一點點的時間 因此還需要再修改一下 ![](https://i.imgur.com/l0vPefv.png) **參考資料** [pthread for raytrcing](https://embedded2016.hackpad.com/ep/pad/static/wOu40KzMaIP)