# 2017q1 Homework1 (raytracing) contributed by <`petermouse`> 溫習去年秋季做的[共筆](https://hackmd.io/s/BkcRGkvp)並繼續改進 ### Reviewed by `hunng` - 利用 OpenMP loop scheduling 去解決原先分配不均的問題,並有顯著效果 - 並在 thread 內加上測試時間去檢查是否有執行時間差異過大的問題 - 可以從系統的角度解釋為何 thread 數從 8 到 128 時效能沒有增長 ## 開發環境 * OS: Ubuntu 16.04 LTS * Memory: 8 GB * CPU: * Name: * Intel(R) Core(TM) i5-4200H CPU @ 2.80GHz * Cache: * L1d Cache: 32K * L1i Cache: 32K * L2 Cache: 256K * L3 Cache: 3072K ## Makefile 每次看 Makefile 都很新奇,以前只用過最基本的,但 Makefile 遠不只這些! 紀錄一下不會的 [(網路參考資料)](http://tetralet.luna.com.tw/?op=ViewArticle&articleId=185) * 可使用條件句: 有 `ifeq` `ifneq` `ifdef` `ifndef`等 if 述句,也可以有對應的 `else`,結尾以`endif` 表示 * 類似 operator 的使用 * `:=` : `=` 內容裡的變數更動後會隨之更動,`:=` 避免這種狀況 * `+=` : 很直覺地代表參數的串接 * `?=` : 沒有 define 才指定 * `%.c`: 所有`.c`結尾的名稱,很像終端機中的 `*` * `$<` : 第一個 dependencies 之值 ( i.e. 冒號後的第一個) * 一堆的字串函式 ([英文](https://www.gnu.org/software/make/manual/html_node/Text-Functions.html),[中文](http://deanjai.blogspot.tw/2008/02/subst-subst-subst-eeeefeet-on-street.html)) * 這次的 `$(strip)` 代表將字串省去開頭結尾的空白 ## raytracing ### 原始版本 make 時不加 `PROFILE=1` 時執行 `raytracing` ``` # Rendering scene Done! Execution time of raytracing() : 2.495271 sec ``` 再 `PROFILE=1` 產生 gprof 的相關資料 (gmon.out) ``` # Rendering scene Done! Execution time of raytracing() : 5.125118 sec ``` 時間要原本的兩倍左右 使用 `gprof` 分析函數間的執行時間以及執行次數 `gprof` 使用時同時包含 image-file 以及 profile-file `$ gprof -b raytracing gmon.out | less` 產生的部份結果如下: ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 22.18 0.47 0.47 69646433 0.00 0.00 dot_product 17.94 0.85 0.38 56956357 0.00 0.00 subtract_vector 11.80 1.10 0.25 10598450 0.00 0.00 normalize 9.68 1.31 0.21 13861875 0.00 0.00 rayRectangularIntersection 7.08 1.46 0.15 31410180 0.00 0.00 multiply_vector 6.37 1.59 0.14 17836094 0.00 0.00 add_vector ``` 多數的時間都發生在數學上的運算,比如前三名 `dot_product()` `subtract_vector()` `normalize` 就佔了 50% ,而且被呼叫的次數極多,從這裡下手對效能也很大的影響 再來觀察 `perf` 對原本程式(未使用 `PROFILE=1`)的結果 ``` Performance counter stats for './raytracing': 121,000 cache-references 41,214 cache-misses # 34.061 % of all cache refs 1,870,361,272 branches 5,298,723 branch-misses # 0.28% of all branches 2.475591343 seconds time elapsed ``` branch 數量竟然高達了18億!branch 除了增加判斷的成本,還有預測錯誤遭造成的懲罰,如果能夠降低 branch 的數量也預期會有更好的表現。 ### 改進方向(1) loop unrolling 在 `math-toolkit.h` 中,有多個 function 內都有固定次數的 for 迴圈 ```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]; } ``` 不是很必要去宣告一個變數 i,判斷 i 是否大於 3,或是對 i 加 1,類似的都可以改成 ```clike= static inline 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]; } ``` 以及最重的 `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; } ``` 可以直接寫成 `return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];` 時間比起最初要快了不少 ``` # Rendering scene Done! Execution time of raytracing() : 1.687605 sec ``` gprof 的結果,有做 loop unrolling 的 function 都把時間壓低了 ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 20.78 0.25 0.25 56956357 0.00 0.00 subtract_vector 13.14 0.40 0.16 69646433 0.00 0.00 dot_product 11.87 0.54 0.14 10598450 0.00 0.00 normalize 9.33 0.65 0.11 13861875 0.00 0.00 raySphereIntersection 7.63 0.74 0.09 4620625 0.00 0.00 ray_hit_object 6.78 0.82 0.08 31410180 0.00 0.00 multiply_vector 6.78 0.90 0.08 13861875 0.00 0.00 rayRectangularIntersection 5.09 0.96 0.06 17836094 0.00 0.00 add_vector 4.66 1.02 0.06 17821809 0.00 0.00 cross_product 4.24 1.07 0.05 1048576 0.00 0.00 ray_color 1.70 1.09 0.02 3838091 0.00 0.00 length 1.70 1.11 0.02 1204003 0.00 0.00 idx_stack_push 1.70 1.13 0.02 1048576 0.00 0.00 rayConstruction 1.27 1.14 0.02 4221152 0.00 0.00 multiply_vectors ``` perf 的結果: ``` Performance counter stats for './raytracing': 92,736 cache-references 36,946 cache-misses # 39.840 % of all cache refs 969,826,972 branches 3,103,684 branch-misses # 0.32% of all branches 1.726622426 seconds time elapsed ``` branch 數直接少一半! 簡單地估算一下,i 每次 function call 需判斷 4 次 ( i = 0 ~ i = 3) ( 56956357 + 69646433 + 31410180 + 17836094 + 4221152 ) * 4 = 720280864 至少少了7億次,實際上少更多 ### 改進方向(2) inline function 看看那些 function 加起來可是呼叫了幾億次!,如果全部 inline 就可以減少呼叫的成本,但缺點是程式碼會很長,增加整體的大小 因為要強制在 `-O0` 的情形下 inline ,在 declarator 前要加 gcc 的 attribute 來強制執行 ```clike= static inline __attribute__((always_inline)) void add_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]; } ``` gprof 中已經看不見這些 function ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 31.55 0.41 0.41 13861875 0.00 0.00 rayRectangularIntersection 29.24 0.79 0.38 13861875 0.00 0.00 raySphereIntersection 16.16 1.00 0.21 2110576 0.00 0.00 compute_specular_diffuse 6.93 1.09 0.09 1048576 0.00 0.00 ray_color 3.08 1.13 0.04 2110576 0.00 0.00 localColor ``` 執行時間少了約 0.2 秒 ``` # Rendering scene Done! Execution time of raytracing() : 1.456480 sec ``` ### 改進方向(3): 使用 OpenMP OpenMP 可以簡單地開啟多個 thread 完成工作,只要在適當的地點加入`pragma omp`,並在編譯連結時加入 `-fopenmp` 即可 程式主體為 `raytracing()` ,再看看 `raytracing()` 內其實由雙層 for 迴圈,對圖像的每一個點(x,y) 依序建立,像素之間沒有什麼相依關係,只要小心有些變數不能共用就好 以 row 為最小單位,對於最外層的迴圈平行化 ```clike= #pragma omp parallel for private(stk, object_color) for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { /* statements */ } } ``` 不過建出來是錯的,呈現就像下圖一樣,還有共用的變數少考慮了! ![](https://i.imgur.com/tB81K1o.jpg) 少讓 `d` 變數變成私有 ```clike= #pragma omp parallel for private(d, stk, object_color) for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { /* statements */ } } ``` 在沒有指定 thread 數量情況下的結果 ``` # Rendering scene Done! Execution time of raytracing() : 0.763289 sec ``` 執行時間降為一半! 考慮不同 thread 數量的執行時間,繪製成下圖 ![](https://i.imgur.com/Cxl94nk.png) 在 2 與 4 個 thread 數量時間是差不多的, 8 以後雖然差不多,但是時間有在稍微將低一點。 參考[2016春季班 team4 共筆](https://embedded2016.hackpad.com/ep/pad/static/f5CCUGMQ4Kp)中的[圖片複雜度分析結果](https://hackpad-attachments.imgix.net/embedded2016.hackpad.com_f5CCUGMQ4Kp_p.575390_1462118230247_out.png?fit=max&w=882),可以發現每個點做的運算量都不太一樣,部份點比起其他點更耗時,看起來是個熱點!如果在平行時能夠 load balancing,就減少 thread 等待其他 thread 的時間,加快整體的速度 #### 嘗試使用 loop scheduling OpenMP loop scheduling 共分為 5 種 * static : 依指定的 chunk size 預先分配每個 thread 的 chunk,未指定 chunk size 為 ( 總 chunk 數量 / 總 thread 數量 ) * dynamic : 動態分配,誰先做完誰就繼續依 chunk size 拿取剩下的 chunk * guided : 類似 dynamic ,不過 thread 取得的 chunk size 依對數遞減,適合複雜度隨迭代指數成長的迴圈 * auto : complier 決定適合使用 static dynamic guided 哪一個 * runtime: 執行時依環境變數 `OMP_SCHEDULE` 決定 static dynamic guided 未說明 loop scheduling 由系統的 `def-sched-var` 決定, `def-sched-var` 的值對應的方法也是 implementation-defined 嘗試使用 static,並將 chunk size 設定為 1,也就是 cyclic partition 程式碼如下 ```clike= #pragma omp parallel for if(OMP_THREADS - 1) num_threads(OMP_THREADS) \ private(d, stk, object_color) schedule(static, 1) for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { /* statements */ } } ``` 與原本的相比 ![](https://i.imgur.com/I7Aoqll.png) 在 4 個 thread 數量的效能增加很多!,原本的圖片在切割成 4 等分是最不平衡的,現在稍微平衡了一點。 其實這裡忘了先確認 default 的分配方式,以上只是猜測 default 直接將 for 迴圈切成等分分配給每個 thread ,才會在 4個 thread 時無法獲得很好的效能 先寫一個小程式看看結果 ```clike= #include <stdio.h> #include <omp.h> int main() { int i = 0; #pragma omp parallel for num_threads(4) for (i = 0; i < 8; i++) { printf("%d:%d\n", omp_get_thread_num(), i); } return 0; } ``` 結果輸出 ``` thread 0 : 0 thread 0 : 1 thread 1 : 2 thread 1 : 3 thread 3 : 6 thread 3 : 7 thread 2 : 4 thread 2 : 5 ``` 看來 default 就如猜想一樣,是直接切割等分分配 chunk 再來驗證 default 4 threads 情形下,是不是因為 thread 2 (熱點最高)拖累整體速度 ```clike= #pragma omp parallel num_threads(OMP_THREADS) { double time1,time2; time1 = omp_get_wtime(); #pragma omp for private(d, stk, object_color) for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { /* statements */ } time2 = omp_get_wtime(); printf("thread %d: %lf\n",omp_get_thread_num(), time2-time1); } } ``` 每一列做完便會輸出 thread id 及累計時間,執行後的最後幾行輸出 ``` thread 2: 0.751149 thread 2: 0.754657 thread 1: 0.755651 thread 2: 0.758141 thread 1: 0.760791 thread 2: 0.762016 thread 2: 0.765384 thread 2: 0.768642 thread 2: 0.771855 thread 2: 0.775030 ``` 其實 thread 0 與 thread 3 (熱點較少)已經先完成了,thread 1 與 thread 2 還在做 > 這樣測量有點不準確,因為 CPU 先挑選哪個 thread 執行也無法確定,不過多次測試情況下都由 thread 2 最後執行完,(static, 1)沒有這個問題 [name=petermouse] 再來試試 dynamic,load balancing 交由程式以一列為單位自動分配 ```clike= #pragma omp parallel for if(OMP_THREADS - 1) num_threads(OMP_THREADS) \ private(d, stk, object_color) schedule(dynamic, 1) for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { ``` ![](https://i.imgur.com/yUkdat3.png) 4 與 8 個 thread 小幅度地改善,其他的與 static 相近 ### 改進方向(4): ## 參考資料 [gnuplot 柱狀圖製作](http://blog.sciencenet.cn/blog-373392-527507.html) [2016春季班 team4 共筆](https://embedded2016.hackpad.com/ep/pad/static/f5CCUGMQ4Kp) [omp for scheduling](http://jakascorner.com/blog/2016/06/omp-for-scheduling.html) [openmp loop scheduling](https://software.intel.com/en-us/articles/openmp-loop-scheduling)