# 2016q1 Homework2 (A) raytracing **<u>作業要求 (A)</u>** * 在 GitHub 上 fork [raytracing](https://github.com/embedded2016/raytracing),並思考如何改善程式效能 * 以 make PROFILE=1 重新編譯程式碼,並且學習 [gprof](https://sourceware.org/binutils/docs/gprof/) * 參考 [使用 GNU gprof 進行 Linux 平台的程式分析](http://os.51cto.com/art/200703/41426.htm) * 以 [gprof](https://sourceware.org/binutils/docs/gprof/) 指出效能瓶頸,並且著手改寫檔案 math-toolkit.h 在內的函式實做,充分紀錄效能差異在共筆 * 注意: 請勿更動編譯器最佳化選項 -O0 (關閉最佳化) * 檔案 math-toolkit.h 定義的若干數學操作函式很重要,可參考 [SIMD optimized dot and cross product functions](http://tomjbward.co.uk/simd-optimized-dot-and-cross/) 和 [2015q3 Homework #1 Ext](http://wiki.csie.ncku.edu.tw/embedded/2015q3h1ext) * 可善用 POSIX Thread, OpenMP, software pipelining, 以及 loop unrolling 一類的技巧來加速程式運作 * 將你的觀察、分析,以及各式效能改善過程,並善用 gnuplot 製圖,紀錄於「[作業區](https://embedded2016.hackpad.com/2016q1-Homework-2-v37rXPJjRlW)」,記得標注「開發紀錄(A)」 安裝開發工具: * $ sudo apt-get update * $ sudo apt-get install graphviz * $ sudo apt-get install imagemagick 再來是取得程式碼: * git clone [](https://github.com/embedded2016/raytracing.git)https://github.com/embedded2016/raytracing.git  嘗試編譯執行,結果如下: ``` # Rendering scene Done! Execution time of raytracing() : 3.285271 sec ``` **<u>利用gprof來進行分析</u>** 編譯執行: * make PROFILE=1 * ./raytracing 程式執行時間: ``` # Rendering scene Done! Execution time of raytracing() : 6.851365 sec ``` 再來是觀察gprof,先將結果輸出去一個檔案,方便之後對比,而且輸出去檔案裏的資料還有顏色,看了比較舒服,也方便查看 * gprof raytracing gmon.out > ori.txt * Flat profile(只貼上一部分): * Each sample counts as 0.01 seconds. * %   cumulative   self              self     total            * time   seconds   seconds    calls   s/call   s/call  name   * 19.58      0.55     0.55 69646433     0.00     0.00  dot_product * 16.74      1.02     0.47 56956357     0.00     0.00  subtract_vector * 10.33      1.31     0.29 31410180     0.00     0.00  multiply_vector * 9.61      1.58     0.27 13861875     0.00     0.00  rayRectangularIntersection * 8.01      1.81     0.23 17836094     0.00     0.00  add_vector * 6.05      1.98     0.17 10598450     0.00     0.00  normalize * 5.70      2.14     0.16 13861875     0.00     0.00  raySphereIntersection * 5.70      2.30     0.16  4620625     0.00     0.00  ray_hit_object * 5.34      2.45     0.15 17821809     0.00     0.00  cross_product * 2.85      2.53     0.08  1048576     0.00     0.00  ray_color * 2.49      2.60     0.07  2520791     0.00     0.00  idx_stack_top * 2.14      2.66     0.06  4221152     0.00     0.00  multiply_vectors * 1.78      2.71     0.05  2110576     0.00     0.00  compute_specular_diffuse * 1.42      2.75     0.04  2110576     0.00     0.00  localColor 可以很明顯的看見dot_product佔了最多時間,被call了近7千萬次,在來是subtract也是佔了大部分時間。 **<u>優化</u>** 大致上瞭解後,就是來進行優化啦。 首先嘗試 <u>loop unrolling</u>,將math-toolkit.h裏的所有for 展開。 優點: * [分支預測](https://zh.wikipedia.org/wiki/%E5%88%86%E6%94%AF%E9%A0%90%E6%B8%AC)(**branch predictor)**失敗減少。 * 如果循環體內語句沒有數據相關,增加了並發執行的機會。 * 可以在執行時動態循環展開。 * 參考資料:[](https://zh.wikipedia.org/wiki/%E5%BE%AA%E7%8E%AF%E5%B1%95%E5%BC%80)https://zh.wikipedia.org/wiki/%E5%BE%AA%E7%8E%AF%E5%B1%95%E5%BC%80 以dot_product爲例子: ```clike= static inline 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; } ``` 優化後的執行時間: ``` # Rendering scene Done! Execution time of raytracing() : 5.835674 sec ``` 降低了不少! 再來是利用gprof分析 * gprof raytracing gmon.out > loop_unrolling.txt * time   seconds   seconds    calls   s/call   s/call  name     * 16.79      0.26     0.26 69646433     0.00     0.00  dot_product * 15.14      0.49     0.23 56956357     0.00     0.00  subtract_vector * 9.87      0.64     0.15 13861875     0.00     0.00  rayRectangularIntersection * 9.22      0.78     0.14 17821809     0.00     0.00  cross_product * 8.23      0.90     0.13 17836094     0.00     0.00  add_vector * 6.58      1.00     0.10 13861875     0.00     0.00  raySphereIntersection * 6.58      1.10     0.10 10598450     0.00     0.00  normalize * 5.60      1.19     0.09  4620625     0.00     0.00  ray_hit_object 可以發現dot_product跟subtract_vector的時間都降低了不少! **<u>SIMD optimized</u>** 嘗試利用SIMD指令來優化dot_product跟cross_product。這裏我先嘗試使用AVX指令來實做dot_product。會使用AVX而不是SSE是因爲它支援256bits,足夠可以儲存4個double,運算也能達到4個。 使用AVX需要在編譯時加上 -mavx2,因此就到makefile加上-mavx2 ``` -std=gnu99 -Wall -O0 -g -mavx2 ``` 更改的程式碼如下: ```clike= double dot_product(const double *v1, const double *v2) { __m256d x = _mm256_loadu_pd((__m256d *)v1); __m256d y = _mm256_loadu_pd((__m256d *)v2); __m256d xy = _mm256_mul_pd(x,y); return (xy[0]+xy[1]+xy[2]); } ``` 執行結果如下: ``` # Rendering scene Done! Execution time of raytracing() : 9.886427 sec ``` 觀察gprof(使用loadu): * gprof raytracing gmon.out > AVX_loadu.txt * time   seconds   seconds    calls   s/call   s/call  name     * 48.12      2.01     2.01 69646433     0.00     0.00  dot_product 嗯時間加長了不少,上網查了一下發現loadu的效率不是很好,所以就不使用loadu,改用別的方法(換成直接set每個值): ```clike= double dot_product(const double *v1, const double *v2) { __m256d x = _mm256_set_pd(0,v1[2],v1[1],v1[0]); __m256d y = _mm256_set_pd(0,v2[2],v2[1],v2[0]); __m256d xy = _mm256_mul_pd(x,y); return (xy[0]+xy[1]+xy[2]); } ``` 換成set後的結果如下(時間加快了不少,可見loadu的效率是真的不好): ``` # Rendering scene Done! Execution time of raytracing() : 8.460185 sec ``` 觀察gprof(使用set): * gprof raytracing gmon.out > AVX_set.txt * time   seconds   seconds    calls   s/call   s/call  name     * 25.88      0.82     0.82 69646433     0.00     0.00  dot_product **<u>Inline</u>** * math-toolkit.h中只是建議compiler將function設定爲inline function,因爲是建議,所以compiler可以不管,因此就將它改成強制inline,以dot_product爲例子: ```clike= static inline __forceinline double dot_product(const double *v1, const double *v2) { double dp = 0.0; dp += v1[0] * v2[0]; dp += v1[1] * v2[1]; dp += v1[2] * v2[2]; return dp; } ``` 修改Makefile,加入: `-D__forceinline="__attribute__((always_inline))" \` **<u>Thread 優化</u>** 先來搞懂Thread: 為了說明何謂 thread ,我們必須先瞭解 UNIX process 與 Mach task 、 thread 間的關係。在 UNIX 中,一個 process 包括了一個執行中的程式,和一些他所需的系統資源,諸如檔案描述表和位址空間等。但是在 Mach 中,一個 task 卻只包括了一些系統資源; 而由thread 掌握了所有的執行活動。一個 Mach task 可能有任意多個 thread , 而 Mach 系統中所有的 thread 均屬於一些 task。屬於同一個 task 的所有 thread 共享該 task 所擁有的系統資源。因此, thread 實質上就是一 個程式計數器、一個堆疊再加上一組暫存器。 UNIX 的一個 process 可以看成是一個 只有一個 thread 的 Mach task。對 CPU 而言,產生一個 thread 是一件相對便宜的工作。另一方面,共享系統資源的 thread 跟獨佔系統資源的 process 比起來,thread 是也是相當節省記憶體的。 * 參考資料:[](http://www.csie.ntu.edu.tw/~r92094/c++/pthread.tx)http://www.csie.ntu.edu.tw/~r92094/c++/pthread.tx 再來就是學一些function: ``` pthread_t :宣告 pthread_create : 建立thread pthread_equal : 判斷是thread是否相同 pthread_join:讓一個thread等待另一個指定thread直到它結束,也可以用來讓主程式等待thread ``` <u>實做:</u> 研究原始碼了一段時間,決定在main.c中宣告thread來使用 ` pthread_t THREAD[4];` 再來是傳入raytracing的參數,由於參數多於一個,因此需要包裝成一個結構才能傳入,所以就另外建立一個結構放在raytracing.h,讓main.c跟raytracing.c都能使用: ```clike= typedef struct __ARG { uint8_t *pixels; color background; rectangular_node rectangulars; sphere_node spheres; light_node lights; const viewpoint *View; int row; int col; } arg; ``` 初始化結構: ```clike= arg data; data.lights = NULL; data.rectangulars = NULL; data.spheres = NULL; data.View = &view; data.col = COLS; data.row = ROWS; data.pixels = malloc(sizeof(unsigned char) * ROWS * COLS * 3); ``` 到了這裏發現程式會編譯不過,發現是use-models.h裏的append的問題,因爲這裏看不太懂code是怎樣運作的,所以不太會改,就用了別的方法,先將append需要用到的變數名稱宣告好,再進行append,最後再給data: ```clike= light_node lights = NULL; rectangular_node rectangulars = NULL; sphere_node spheres = NULL; #include "use-models.h" data.lights = lights; data.rectangulars = rectangulars; data.spheres = spheres; ``` 再來是建立thread: ```clike= for(int i=0; i<4; i++) { if(pthread_create(&THREAD[i],NULL,(void*)raytracing,(void*)&data)) { printf("ERROR thread create!\n"); pthread_exit(NULL); } } for(int i=0; i<4; i++) pthread_join(THREAD[i],NULL); ``` 修改在raytracing.c中的raytracing,將col 切成4等份,利用4個thread來做運算。修改完後發現各個thread會覆蓋掉之前還沒使用完畢的變數(就是切成4等份的col),在這裏卡了很久,決定將thread宣告在raytracing.h中,當作全域變數來使用。 再利用pthread_equal來判斷目前是哪個thread,就用不同的變數來實做: ```clike= if(pthread_equal(pthread_self(),THREAD[0])) { start_j = 0; end_j = height / 4; } ``` 都修改完畢後,結果如下: ``` # Rendering scene Done! Execution time of raytracing() : 1.018773 sec ``` 時間降低到了1.01秒!