# 2017q1 Homework1 (raytracing) contributed by < `etc276` > ###### tags: `嵌入式` >>* 請加快進度 >>* 記得補上硬體相關資訊 >>[name=課程助教][color=red] ### Reviewed by `illusion030` * 可以嘗試 openMP 不同的 schedule 實作不同的平行化策略,可參考[ryanwang522的共筆](https://hackmd.io/s/BJsz9RZqx#multi-thread) * 可以把實驗結果畫出比較圖,試著跟編譯器最佳化比較看看 ## 開發環境 - Ubuntu 16.10 (64bit) ``` 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) i7-5500U CPU @ 2.40GHz Stepping: 4 CPU MHz: 2400.878 CPU max MHz: 3000.0000 CPU min MHz: 500.0000 BogoMIPS: 4788.90 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 4096K NUMA node0 CPU(s): 0-3 ``` ## 相關連結 - [2017 年春季作業說明](https://hackmd.io/s/Bk-1zqIte#) - [2017q1 Homework1 (作業區)](https://hackmd.io/MYJjEYGZITgWgOwCMAmw4BYYwAxyQQKxwBmwGOkAHMDeEgGxA===?view) - [B02: raytracing作業要求](https://hackmd.io/s/HyuBWDwYl) - [raytracing Github連結 ( etc276 ) ](https://github.com/etc276/raytracing) - [作業解說 video](https://www.youtube.com/watch?v=m1RmfOfSwno) - [參考實做程式的解說 video](https://www.youtube.com/watch?v=V_rLyzecuaE) - [課程進度與開放資源 Wiki](http://wiki.csie.ncku.edu.tw/sysprog/schedule) ## 開發紀錄 ### Original * 先 make 然後 ./raytracing 看看,得到 ```clike # Rendering scene Done! Execution time of raytracing() : 2.655160 sec ``` ![](https://i.imgur.com/4Zl4Bxq.png) * 再來使用 gprof 查看效能瓶頸 ```clike $ make clean $ make PROFILE=1 $ ./raytracing $ gprof ./raytacing | less ``` ```clike Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 24.46 0.56 0.56 69646433 0.00 0.00 dot_product 22.04 1.06 0.50 56956357 0.00 0.00 subtract_vector 8.82 1.26 0.20 17821809 0.00 0.00 cross_product 8.38 1.45 0.19 31410180 0.00 0.00 multiply_vector 6.61 1.60 0.15 10598450 0.00 0.00 normalize 6.17 1.74 0.14 13861875 0.00 0.00 rayRectangularIntersection 4.85 1.85 0.11 4620625 0.00 0.00 ray_hit_object 3.97 1.94 0.09 17836094 0.00 0.00 add_vector 3.53 2.02 0.08 13861875 0.00 0.00 raySphereIntersection 3.09 2.09 0.07 1048576 0.00 0.00 ray_color 1.98 2.13 0.05 4221152 0.00 0.00 multiply_vectors 1.32 2.16 0.03 2110576 0.00 0.00 localColor 1.32 2.19 0.03 1 0.03 2.27 raytracing 0.88 2.21 0.02 2110576 0.00 0.00 compute_specular_diffuse 0.88 2.23 0.02 1048576 0.00 0.00 rayConstruction 0.44 2.24 0.01 3838091 0.00 0.00 length 0.44 2.25 0.01 1241598 0.00 0.00 protect_color_overflow 0.44 2.26 0.01 1204003 0.00 0.00 idx_stack_push 0.44 2.27 0.01 1048576 0.00 0.00 idx_stack_init 0.00 2.27 0.00 2558386 0.00 0.00 idx_stack_empty 0.00 2.27 0.00 2520791 0.00 0.00 idx_stack_top 0.00 2.27 0.00 1241598 0.00 0.00 reflection 0.00 2.27 0.00 1241598 0.00 0.00 refraction 0.00 2.27 0.00 113297 0.00 0.00 fresnel 0.00 2.27 0.00 37595 0.00 0.00 idx_stack_pop 0.00 2.27 0.00 3 0.00 0.00 append_rectangular 0.00 2.27 0.00 3 0.00 0.00 append_sphere 0.00 2.27 0.00 2 0.00 0.00 append_light 0.00 2.27 0.00 1 0.00 0.00 calculateBasisVectors 0.00 2.27 0.00 1 0.00 0.00 delete_light_list 0.00 2.27 0.00 1 0.00 0.00 delete_rectangular_list 0.00 2.27 0.00 1 0.00 0.00 delete_sphere_list 0.00 2.27 0.00 1 0.00 0.00 diff_in_second 0.00 2.27 0.00 1 0.00 0.00 write_to_ppm % the percentage of the total running time of the ``` * 發現最花時間的是 ```subtract_vector``` 和 ```dot_product``` ```clike== static inline double dot_product(const double *v1, const double *v2) { double dp = 0.0; for (int i = 0; i < 3; i++) dp += v1[i] * v2[i]; return dp; } ``` ```clike== static inline void subtract_vector(const double *a, const double *b, double *out) { for (int i = 0; i < 3; i++) out[i] = a[i] - b[i]; } ``` ### Optimization 1 (by loop unrolling) 迴圈的用意為,重複執行差異不大的指令,這樣的好處是可以增加程式的可讀性,也可以在執行時期才決定要重複幾次。但如果我們事先就知道迴圈要執行的次數,就可以手動展開,達到 prepocessing 的效果以減少時間,缺點是可讀性會降低。 如下所示,將 math-toolkit.h 裡的 function 都用相同方法改寫。 ```clike== static inline double dot_product(const double *v1, const double *v2) { return (v1[0] * v2[0]) + (v1[1] * v2[1]) + (v1[2] * v2[2]); } ``` 效果顯著,時間從 2.6 秒 降為 1.8 秒。 ``` # Rendering scene Done! Execution time of raytracing() : 1.809386 sec ``` ### Optimization 2 (by force inline) 手動展開迴圈之後,效能瓶頸就卡在 function call 所需的時間。因為每當要呼叫 function 時,所以下一個優化方法就選用可以將 function 在 compile 階段就拆成 instruction cache 的 force inline 以加速,缺點是若函式的代碼過多,會提升 cache miss. 如下所示,將 math-toolkit.h 裡的 function 都用相同方法改寫。要注意的是像 ```scalar_triple_product``` 這種有多層呼叫的函式不要 force inline. ```clike== static inline __attribute__((always_inline)) double dot_product(const double *v1, const double *v2) { return (v1[0] * v2[0]) + (v1[1] * v2[1]) + (v1[2] * v2[2]); } ``` 時間又下降了約 0.3 秒。 ``` # Rendering scene Done! Execution time of raytracing() : 1.518595 sec ``` ### Optimization 3 (by OpenMP) 降低 function call 對效能的影響之後,試圖把任務分給多核心去做處理,以降低 real time,這邊選用 OpenMP 的 API 並挑選 raytracing 的其中一段 loop 實現。要注意的是要去 Makefile 裡兩個 FLAG 的後面多上一段指令 ``` -fopenMP``` ```c= int j, i, s; double r, g, b; #pragma omp parallel for private (i, r, g, b, s, object_color, stk, d) for (j = 0; j < height; j++) { for (i = 0; i < width; i++) { r = 0, g = 0, b = 0; /* MSAA */ for (s = 0; s < SAMPLES; s++) { idx_stack_init(&stk); rayConstruction(d, u, v, w, ``` 參考 [簡易的程式平行化-OpenMP(五) 變數的平行化](https://kheresy.wordpress.com/2006/09/22/%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%94%EF%BC%89-%E8%AE%8A%E6%95%B8%E7%9A%84%E5%B9%B3%E8%A1%8C%E5%8C%96/)後得知要將變數平行化,否則會產生非預期結果的情況,所以將迴圈中的變數都設為 private. 時間減少約0.6秒,效果顯著。 ``` # Rendering scene Done! Execution time of raytracing() : 0.977788 sec ``` ## 相關知識 ### gprof 在使用 gprof 檢查效能瓶頸時,遇到跟 [FB 討論區](https://www.facebook.com/groups/system.software2017/permalink/1325126694219363/)一樣的問題,在參考了[劉俊林的共筆](https://hackmd.io/s/B1hlfxl9e#)之後才解決。 ### inline v.s. macro C99的標準新增了 inline function 的定義: >> Making a function an inline function suggests that calls to the function be as fast as possible. >> 值得注意的是 suggest 這個字,加上 inline 的關鍵字只是建議編譯器可以這樣做,但是因為我們的 makefile 中將最佳化關閉 (-O0),所以如果不 force inline 則所有建議將不被採用。 macro 和 inline 的區別在於,前者是在 prepocess 階段將程式碼 (source code) 代換成 #define 後面的模樣,所以編譯器並不會幫你檢查傳入參數的型別,而且也因為只是代換,所以在巨集被呼叫多次之後,會存放使用大量的記憶體空間;後者是在 complie 階段將 function 轉成 instruction cache,因此可以減去 function 呼叫時,程式跑到函式所在的記憶體位置的執行時間。 #### 參考資料 [深入理解内联inline函数的优缺点,性能及使用指南](http://blog.csdn.net/acs713/article/details/42649949) [Enhance raytracing program](https://embedded2016.hackpad.com/ep/pad/static/f5CCUGMQ4Kp) [macro與inline差異](http://hugedream2.blogspot.tw/2008/09/macroinline.html) ## 心得 以前就有聽過 function call 會佔用不少時間,所以建議使用 gdb 等 debug 的方法,而不是 ```printf()```等呼叫函式以檢查輸出。但是直到這次作業才知道效能上的差異,也學習到使用 inline 等優化方法,但目前對於 OpenMP, PTread 等方法還不盡理解,仍待補實際的 code 以分析效能。 :::danger 真正問題在於太少動手了,你要想想自己對這個世界的影響,活了二十多年,結果這個世界裡頭,誰用了你開發的程式碼?請重新認真看待你和這個世界的關聯,持續提升軟體品質 --jserv :::