# 2017q1 Homework1 (raytracing) contributed by < `laochanlam` > ###### tags: `laochanlam` ### Reviewed by `Daichou` * Makefile中強制指派gcc版本是否為好事(Makefile:33)? * 應該討論OpenMP在不同核心數與執行序數的情況下的效能比較 * 應該嘗試使用不同的openmp schedule方式來測試效能 ## 開發環境 - 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連結 ( laochanlam ) ](https://github.com/laochanlam/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) ## Raytracing 開發紀錄 在看完解說 video 後,開始依作業的要求嘗試使用 gprof 分析 raytracing 的效能。 因老師已經在Makefile中加入了 ```-pg``` 的指令,所以我們只需在make的時候輸入 ```$ make PROFILE=1``` 編譯後即可使用 gprof ,然後輸入 ```$ gprof raytracing | less``` 發現... ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls Ts/call Ts/call name ``` 沒有結果!!! 然後發現[FB討論區](https://www.facebook.com/groups/system.software2017/permalink/1325126694219363/)有人遇到了同樣的問題.. 傳說中好像是gcc版本的問題... ### 解決過程: 先查看自己安裝的 gcc 版本 ```$ gcc -v``` ``` gcc version 6.2.0 20161005 (Ubuntu 6.2.0-5ubuntu12) ``` 然後參考[FB討論區](https://www.facebook.com/groups/system.software2017/permalink/1325126694219363/)中翁同學的方法,安裝 gcc5 ```$ sudo apt-get install gcc-5``` ,然後在Makefile中把 ./raytracing 的輸出部分的 ``` $(CC)``` 改成 ```gcc-5 -no-pie``` 。 **解決了!** ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 25.75 0.70 0.70 69646433 0.00 0.00 dot_product 20.60 1.26 0.56 56956357 0.00 0.00 subtract_vector 9.93 1.53 0.27 13861875 0.00 0.00 rayRectangularIntersection 9.20 1.78 0.25 31410180 0.00 0.00 multiply_vector 7.36 1.98 0.20 17836094 0.00 0.00 add_vector 6.62 2.16 0.18 10598450 0.00 0.00 normalize 4.41 2.28 0.12 17821809 0.00 0.00 cross_product 3.68 2.38 0.10 4620625 0.00 0.00 ray_hit_object 3.13 2.47 0.09 13861875 0.00 0.00 raySphereIntersection 2.58 2.54 0.07 4221152 0.00 0.00 multiply_vectors 1.84 2.59 0.05 1048576 0.00 0.00 ray_color 1.47 2.63 0.04 2110576 0.00 0.00 compute_specular_diffuse 0.92 2.65 0.03 2520791 0.00 0.00 idx_stack_top 0.74 2.67 0.02 3838091 0.00 0.00 length 0.74 2.69 0.02 2110576 0.00 0.00 localColor 0.74 2.71 0.02 1 0.02 2.72 raytracing 0.37 2.72 0.01 1241598 0.00 0.00 refraction 0.00 2.72 0.00 2558386 0.00 0.00 idx_stack_empty 0.00 2.72 0.00 1241598 0.00 0.00 protect_color_overflow 0.00 2.72 0.00 1241598 0.00 0.00 reflection 0.00 2.72 0.00 1204003 0.00 0.00 idx_stack_push 0.00 2.72 0.00 1048576 0.00 0.00 idx_stack_init 0.00 2.72 0.00 1048576 0.00 0.00 rayConstruction 0.00 2.72 0.00 113297 0.00 0.00 fresnel 0.00 2.72 0.00 37595 0.00 0.00 idx_stack_pop 0.00 2.72 0.00 3 0.00 0.00 append_rectangular 0.00 2.72 0.00 3 0.00 0.00 append_sphere 0.00 2.72 0.00 2 0.00 0.00 append_light 0.00 2.72 0.00 1 0.00 0.00 calculateBasisVectors 0.00 2.72 0.00 1 0.00 0.00 delete_light_list 0.00 2.72 0.00 1 0.00 0.00 delete_rectangular_list 0.00 2.72 0.00 1 0.00 0.00 delete_sphere_list 0.00 2.72 0.00 1 0.00 0.00 diff_in_second 0.00 2.72 0.00 1 0.00 0.00 write_to_ppm ``` 至於 ```-no-pie``` 是什麼這個晚點再來研究。 接下來要正式進入主題了,先來理解一下整個專案的架構。 由上邊 gporf 的輸出結果可以看得到 ```dot_product```及```subtract_vector```被呼叫得最多,所以從這兩個函式開始著手。 來看看原來程式執行所需的時間吧! ``` # Rendering scene Done! Execution time of raytracing() : 2.673156 sec ``` 大概是2.6秒左右。 然後在執行得最多的函式 ```dot_product``` 及 ```subtract_vector``` 當中發現, ```C 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; } ``` ```C 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]; } ``` 可進行最簡單的 **loop unrolling** ,接著就把 math-toolkit.h 中的函式都用 loop unrolling的方法展開。 ## 優化1: loop unrolling ``` # Rendering scene Done! Execution time of raytracing() : 1.803709 sec ``` 效果拔群!變成1.8秒了! 再來用 gprof 看一下最上邊的幾個函式。 ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 18.10 0.28 0.28 69646433 0.00 0.00 dot_product 14.81 0.50 0.23 56956357 0.00 0.00 subtract_vector 13.17 0.70 0.20 13861875 0.00 0.00 rayRectangularIntersection 9.22 0.84 0.14 31410180 0.00 0.00 multiply_vector 7.24 0.95 0.11 17836094 0.00 0.00 add_vector 6.91 1.06 0.11 17821809 0.00 0.00 cross_product .......... ``` 雖然 call 的次數沒有變,可是總執行時間變少了。 ## 優化2: inline 因為在 math-toolkit.h 中看到 **static inline** 這個之前沒見過的關鍵字,所以在閱讀完inline的資料後,以及參考完[參考實做程式的解說 video](https://www.youtube.com/watch?v=V_rLyzecuaE) 後,得知 inline 原來在被關閉編譯器最佳化 ```-O0``` 後是不會被執行的,所以我們加入讓編譯器可以強執執行 inline function 的程式碼(詳見下方相關資料)試試看可不可以加快執行速度。 ``` # Rendering scene Done! Execution time of raytracing() : 1.590904 sec ``` 成功了!然後用 gprof 要驗証一下函式是否真的被展開。 ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 32.87 0.46 0.46 13861875 0.00 0.00 rayRectangularIntersection 19.29 0.73 0.27 13861875 0.00 0.00 raySphereIntersection 15.00 0.94 0.21 2110576 0.00 0.00 compute_specular_diffuse 9.29 1.07 0.13 1048576 0.00 0.00 ray_color 5.72 1.15 0.08 4620625 0.00 0.00 ray_hit_object ....... ``` 從最上邊幾個花費時間最多的函式中,發現 math-toolkit.h 中的幾個函式都 不 見 了 ! 被 展 開 了 ! ## 優化3: OpenMP 接下來嘗試使用 OpenMP 進行優化。 在研究 **raytracing.c** 的時候,找到了以下程式碼。 ```C for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { double r = 0, g = 0, b = 0; /* MSAA */ for (int s = 0; s < SAMPLES; s++) { idx_stack_init(&stk); rayConstruction(d, u, v, w, i * factor + s / factor, j * factor + s % factor, view, width * factor, height * factor); if (ray_color(view->vrp, 0.0, d, &stk, rectangulars, spheres, lights, object_color, MAX_REFLECTION_BOUNCES)) { r += object_color[0]; g += object_color[1]; b += object_color[2]; } else { r += background_color[0]; g += background_color[1]; b += background_color[2]; } pixels[((i + (j * width)) * 3) + 0] = r * 255 / SAMPLES; pixels[((i + (j * width)) * 3) + 1] = g * 255 / SAMPLES; pixels[((i + (j * width)) * 3) + 2] = b * 255 / SAMPLES; } } } ``` 乍看來每次 loop 都沒有相依性,所以嘗試對這個 for 進行平行化的處理。 ``` # Rendering scene Done! Execution time of raytracing() : 0.871998 sec ``` 然後 ```$ make check```就 fail 了... 果然事情並沒有這麼簡單。 <br> 在閱讀完 [king1224大大 的共筆](https://hackmd.io/EYUwTAnMEQbAtBAZgdifALCkJ4EMwBmCeAVgA4QBGEDPWUjEUoA=#)後,發現了就算參數之間沒有相依性,也有可能在多個執行緒中被同時改變著,所以參考了其程式碼的寫法,把部份參數給于```private``` 的屬性。 新版本: ```C int i,j,s; double r,g,b; #pragma omp parallel for private(i,s,r,g,b,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, i * factor + s / factor, j * factor + s % factor, view, width * factor, height * factor); if (ray_color(view->vrp, 0.0, d, &stk, rectangulars, spheres, lights, object_color, MAX_REFLECTION_BOUNCES)) { r += object_color[0]; g += object_color[1]; b += object_color[2]; } else { r += background_color[0]; g += background_color[1]; b += background_color[2]; } pixels[((i + (j * width)) * 3) + 0] = r * 255 / SAMPLES; pixels[((i + (j * width)) * 3) + 1] = g * 255 / SAMPLES; pixels[((i + (j * width)) * 3) + 2] = b * 255 / SAMPLES; } } } } ``` 這次 verify 終於成功了,而且時間也被大量縮減。 ``` # Rendering scene Done! Execution time of raytracing() : 0.795355 sec ``` <br> 最後來個效能對比圖作結 ![Imgur](http://i.imgur.com/FSiSgOy.png =800x) --- ## 補充知識 - [x] gprof - [ ] POSIX Thread - [x] OpenMP - [x] software pipelining - [x] loop unrolling - [x] static inline - [x] marco --- ## 程式性能分析工具: gprof 是一個基於 Linux 下方便易用效能分析的工具,會輸出程式中 function 被 call 的次數及時間等重要資訊,如上方開發紀錄,但缺點是會加入一些額外的程式碼使執行時間變長。 用法很簡單,只要在編譯時加入``` -pg ```的參數,然後```$ gprof [執行檔]```即可觀看分析資訊。 #### 參考及引用資料 [使用Gnu gprof进行Linux平台下的程序分析](http://os.51cto.com/art/200703/41426.htm) --- ## Loop unrolling 犧性程式碼的可讀性及行數來換取程式執行速度的一種優化方法。 例子如下圖: ```C for (i = 1; i <= 60; i++) a[i] = a[i] * b + c; ``` 可以展開成 ```C for (i = 1; i <= 60; i+=3) { a[i] = a[i] * b + c; a[i+1] = a[i+1] * b + c; a[i+2] = a[i+2] * b + c; } ``` 這被稱作loop unrolling。 #### 參考及引用資料 [循环展开 Wiki](https://zh.wikipedia.org/wiki/%E5%BE%AA%E7%8E%AF%E5%B1%95%E5%BC%80) --- ## OpenMP 依我的理解是一套讓程式設計師可以簡單地實現程式多執行緒化的API。 例子如下圖: ```C #include <omp.h> #include <stdio.h> #include <stdlib.h> void Test( int n ) { for( int i = 0; i < 10000; ++ i ) { //do nothing, just waste time } printf( "%d, ", n ); } int main(int argc, char* argv[]) { #pragma omp parallel for for( int i = 0; i < 10; ++ i ) Test( i ); system( "pause" ); } ``` 這段簡單的程式碼加入了 ```#include <omp.h>``` 及 ```#pragma omp parallel for```即完成了多執行緒的實現,十分輕便。 輸出結果: ``` 0, 5, 1, 6, 2, 7, 3, 8, 4, 9, ``` 使用多執行緒處理程式可使程式的效能提高,但要小心有些程式碼並不能隨便更改執行順序。 > private 讓每個執行緒中,都有一份變數的複本,以免互相干擾。 Specifies that each thread should have its own instance of a variable. >shared 將變數設定為各執行緒共用(應該算是相對於 private 的)。 Specifies that one or more variables should be shared among all threads. <br> ```C #pragma omp parallel num_threads(THREAD_CNT) ``` 控制要進行平行化所用的執行緒數量。 ```C shared(height,width,factor) private(stk,d,object_color) ``` 把參數 ```height,width,factor``` 設定為被執行緒共用, 把參數 ```stk,d,object_color``` 設定為執行緒緒手一份。 #### 參考及引用資料 [簡易的程式平行化方法-OpenMP(一)簡介](https://kheresy.wordpress.com/2006/06/09/%E7%B0%A1%E6%98%93%E7%9A%84%E7%A8%8B%E5%BC%8F%E5%B9%B3%E8%A1%8C%E5%8C%96%E6%96%B9%E6%B3%95%EF%BC%8Dopenmp%EF%BC%88%E4%B8%80%EF%BC%89%E7%B0%A1%E4%BB%8B/) [簡易的程式平行化-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/) [簡易的程式平行化-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/) [OpenMP Wiki](https://zh.wikipedia.org/zh-hk/OpenMP) --- ## inline 使用內嵌函數( inline function )是一種加快程式執行速度的方法,當我們加入關鍵字時,就像是給編譯器"建議",在編譯時把函數中的程式直接展開,以換取速度,省去了許多的呼叫函數的時間。 注意: 加入了關閉編譯器最佳化 ```-O0``` 後編譯器是不會執行inline的。 #### 參考及引用資料 [内联函数:static inline 和 extern inline 的含义](http://www.cnblogs.com/pengyingh/articles/2405718.html) [內嵌函數(inline function)筆記](https://dotblogs.com.tw/v6610688/2013/11/27/introduction_inline_function) [强制函数inline | 100个gcc小技巧](https://wizardforcel.gitbooks.io/100-gcc-tips/content/must-forceinline-function-code.html) ---