# 2016 homework2(A) Raytracing
## 作業要求 A
* 在 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 一類的技巧來加速程式運作
## 學習Gprof
* 在gcc 編譯和連結你的程式的時候(使用-pg編譯和連結選項),gcc在你程式的每個函式中都加入了一個名為mcount ( or “_mcount” , or “__mcount ” ,依賴於編譯器或作業系統)的函式,也就是說你的程式裡的每一個函式都會呼叫mcount函式,而mcount會在內存中保存一張函式呼叫圖,並通過函式呼叫heap 與 stack 的形式查找caller和callee的地址。這張呼叫圖也保存了所有與函式相關的呼叫時間,呼叫次數等等的所有信息。
* 在這次作業中可以看到Makefile 有下面的指令:
```
ifeq ($(strip $(PROFILE)),1)
PROF_FLAGS = -pg
CFLAGS += $(PROF_FLAGS)
LDFLAGS += $(PROF_FLAGS)
endif
```
**ifeq ($(strip $(PROFILE)),1)** 這段也可以這樣表示**ifeq (a,a)** 或是**ifeq "a" "a"** 但為了避免變數展開後包含了非預期的空白字符,所以這樣的寫法比較好,而在使用 make指令時 要求加入 PROFILE=1,才會去比較 **if(value_1,value_2)** 看是否成立 則CFLAGS 加入 -pg flag.
**step1:**
輸入` make PROFILE=1 && ./raytracing` 則會產生一個gmon.out 檔,如果沒有先make clean在輸入一次
**step2:**
`gprof -b ./raytracing gmon.out | less` 由於gprof輸出的信息比較多,這裡使用了less 命令,該命令可以讓我們通過上下方向建查看gprof產生的輸出,| 表示gprof -b a.out gmon.out 的輸出作為less的輸入
```
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
* time seconds seconds calls s/call s/call name
* 24.50 0.60 0.60 69646433 0.00 0.00 dot_product
* 20.83 1.11 0.51 56956357 0.00 0.00 subtract_vector
* 9.19 1.34 0.23 13861875 0.00 0.00 rayRectangularIntersection
* 7.76 1.53 0.19 31410180 0.00 0.00 multiply_vector
* 6.13 1.68 0.15 17836094 0.00 0.00 add_vector
* 5.51 1.81 0.14 13861875 0.00 0.00 raySphereIntersection
* 4.90 1.93 0.12 4620625 0.00 0.00 ray_hit_object
* 3.68 2.02 0.09 1048576 0.00 0.00 ray_color
* 3.27 2.10 0.08 17821809 0.00 0.00 cross_product
可以看到dot_product 這個函式被執行了快7千萬次而subtract_vector執行次數也很高
```
**step3:**
`gprof raytracing gmon.out > analysis.txt` 可以把上面的結果輸出到analysis.txt檔案
## Gprof call graph圖
這邊參考RINC同學筆記
輸入 `make PROFILE=1 && ./raytracing` 會產生gmon.out 檔
之後在輸入`gprof ./raytracing | ~/gprof2dot/gprof2dot.py| dot -Tpng -o callgraph.png`
記得要先去`git clone https://github.com/jrfonseca/gprof2dot.git https://github.com/jrfonseca/gprof2dot.git` **[gprof2dot](https://github.com/jrfonseca/gprof2dot)**
就可以看到函數呼叫情形的關係圖如下:
![](https://hackpad-attachments.s3.amazonaws.com/dslinwork.hackpad.com_COqoFNetQyf_p.628101_1466758807692_2016-06-24%2016-58-33%20%E7%9A%84%E8%9E%A2%E5%B9%95%E6%93%B7%E5%9C%96.png)
## 修改math-toolkit.h
**未優化:**
先以` make PROFIEL=1 && ./raytracing ` 重新編譯並執行
```
* # Rendering scene
* Done
* Execution time of raytracing() : 5.744171 sec
* Each sample counts as 0.01 seconds.
* % cumulative self self total
* time seconds seconds calls s/call s/call name
* 22.52 0.52 0.52 69646433 0.00 0.00 dot_product
* 19.93 0.98 0.46 56956357 0.00 0.00 subtract_vector
* 10.83 1.23 0.25 31410180 0.00 0.00 multiply_vector
* 6.93 1.39 0.16 17821809 0.00 0.00 cross_product
```
沒加-pg:
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 2.430251 sec
```
## 開啟編譯器最佳化-Ofast
有加-pg:
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 0.691856 sec
```
沒有加-pg:
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 0.556231 sec
```
也太快了吧~!
## loop unrolling
原式:
```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;
}
```
可以看到原本是一個for迴圈雖然只短短跑3次,但是此函式跑了7千萬次,所以總共要執行2億次的for迴圈,這邊採用loop unrolling 來減少迴圈的負荷,
```clike=
static inline 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;
}
```
```
# Rendering scene
Done!
Execution time of raytracing() : 4.750202 sec
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
18.76 0.29 0.29 69646433 0.00 0.00 dot_product
13.82 0.50 0.21 56956357 0.00 0.00 subtract_vector
9.87 0.65 0.15 13861875 0.00 0.00 rayRectangularIntersection
9.87 0.80 0.15 4620625 0.00 0.00 ray_hit_object
7.90 0.92 0.12 31410180 0.00 0.00 multiply_vector
7.57 1.03 0.12 17821809 0.00 0.00 cross_product
```
可以看到時間有明顯下降從5.744171 sec -> 4.750202 sec 下降快0.99秒(有加-pg)
**沒加-pg :**
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 1.865293 sec
```
可以看到時間有明顯下降從2.430251 sec -> 1.865293 sec 下降快0.99秒
## inline + loop unrolling
可以看到原本函式都有static inline 但因為我們關閉了最佳化-O0 而且inline 也是建議編譯器要將它設為行內函式,但並不一定會執行,所以參考老師上課提示除非我們加forceinline 告知編譯器這邊要做最佳化,許同學說:inline後的函數不保證不會留外部符號,由於可能有多檔案引用.h會造成重複定義故加static
* 這邊對於同學說的外部符號有點不太懂?
GCC does not inline any functions when not optimizing unless you specify the ‘always_inline’ attribute for the function, like this:
* /* Prototype. */
* inline void foo (const char) __attribute__((always_inline));
The remainder of this section is specific to GNU C90 inlining.
在 Makefile 加入以下:
```
* CFLAGS = \
* -std=gnu99 -Wall -O0 -g \
* -D__forceinline="__attribute__((always_inline))"
```
把函式改成以下:
```clike=
static inline __forceinline double length(const double *v)
{
return sqrt(v[0] * v[0] + v[1] * v[1] + v[2] v[2]);
}
```
```
# Rendering scene
Done!
Execution time of raytracing() : 2.441818 sec
```
時間有明顯下降從 4.750202 sec -> 2.441818 sec 下降快2.31秒
```
* Flat profile:
* Each sample counts as 0.01 seconds.
* % cumulative self self total
* time seconds seconds calls s/call s/call name
* 30.73 0.47 0.47 13861875 0.00 0.00 rayRectangularIntersection
* 26.15 0.87 0.40 13861875 0.00 0.00 raySphereIntersection
* 10.46 1.03 0.16 2110576 0.00 0.00 compute_specular_diffuse
* 9.15 1.17 0.14 1048576 0.00 0.00 ray_color
* 5.23 1.25 0.08 4620625 0.00 0.00 ray_hit_object
可以看到因為都強制展開了所以只剩下其他的函式
**沒加-pg :**
* # Rendering scene
* Done!
* Execution time of raytracing() : 1.652027 sec
```
## Multithread +loop unrolling + inline
先來研究POSIX Thread 如下:
**pthread_create**這個Function的作用是用來產生一個Thread並執行附帶的Function,回傳值: 如果執行成功則回傳0,如果執行失敗則回傳錯誤代碼
```clike=
pthread_create(pthread_t *tid ,//為pthread的指標,在使用Thread之前必須要先宣告一個pthread_t的變數
const pthread_attr_t *attr ,//該Thread的屬性,預設是NULL
void *(*function)(void *) , //為Function pointer,這邊要放入你要執行的Function
void *argument);//為Function pointer所要帶的參數
```
ex: pthread_create( &thread1, NULL , showmessage , message);
**pthread_exit**:用來關閉一個Thread,附帶有1個參數。
原始的定義 void pthread_exit (void *value_ptr)
參數1: void *value_ptr用來設定執行成功時該Thread會回傳的值,這個值可由pthread_join()這個 Function來取得。
回傳值: 不會回傳任何值。
example: pthread_exit(NULL);
**pthread_join:**
原始的定義 int pthread_join (pthread_t thread, void **value_ptr)
這個Function的作用會暫停目前執行pthread_join的Thread,等到目標Thread執行完畢之後 目前執行pthread_join的Thread才會繼續執行,附帶有2個參數。
參數1: pthread_t thread為要等待的目標Thread。
參數2: void **value_ptr用來取得目標Thread的回傳值。
回傳值: 如果執行成功則回傳0,如果執行失敗則回傳錯誤代碼
**<u>實做:</u>**
這邊實做主要參考陳品睿同學筆記架構,可以知道raytracing這個函式可以做平行化,我是把thread create還有宣告這部份都寫在main程式,因為在寫程式的時候會遇到的問題是要如何把raytracing所需要的參數傳進去,而thread只能傳一個參數,所以這邊是用兩個struct 把參數包起來一個包含基本的參數,另一個則是依照thread 1 要做0,4,8.....column, thread 2 做1,5,9.....column 這邊要小心傳遞的參數要仔細,程式修改完後跑完結果圖沒出來,後來找了很久才知道在ratraycing 函式中 for迴圈有個小地方沒有改到結果圖一直沒有顏色@@ 還好有gdb幫忙可以節省很多時間
```clike=
typedef struct _thread_arg{
uint8_t *pixels;
double *background_color;
rectangular_node rectangulars;
sphere_node spheres;
light_node lights;
const viewpoint *view;
int width;
int height;
int thnum;
}thread_arg; //基本參數
typedef struct _thr_arg{
thread_arg *ptr;
int start_height;
}thr_arg;//主要傳遞thread 從那一個column開始做
```
**<u>以column來做分割:</u>**
**thread num:4**
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 1.976252 sec (-pg)
* Execution time of raytracing() : 0.474479 sec (沒有-pg)
```
**thread num:8**
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 1.756253 sec (-pg)
* Execution time of raytracing() : 0.492798 sec (沒有-pg)
```
**thread num:16**
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 1.708510 sec (-pg)
* Execution time of raytracing() : 0.488995 sec(沒有-pg)
```
**thread num:32**
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 1.641048 sec (-pg)
* Execution time of raytracing() : 0.486502 sec (沒有-pg)
```
**thread num:64**
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 1.641311 sec (-pg)
* Execution time of raytracing() : 0.487008 sec(沒有-pg)
```
以上可以看到在沒有加 -pg 時 thread num =4時是最佳的,因為我的電腦是四核心,實際上也是4個執行緒在跑,已經可以比ofast快0.11 秒,還可以在思考哪邊可以在加速~~
**<u>以ROW來做分割:</u>**
**thread num:4**
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 0.506358 sec
```
**thread num:8**
```
* # Rendering scene
* Done!
* Execution time of raytracing() : 0.501141 sec
```
可以看到以column來分割會比較快
![](https://hackpad-attachments.s3.amazonaws.com/dslinwork.hackpad.com_COqoFNetQyf_p.628101_1466779021420_runtime.png)
以上都是沒加-pg的時間可以看到一直優化下去時間相對的一直減少,用multithread 最後竟然可以超越ofast 真是太厲害了~~ 果然善用好的方法是可以大幅改善效能的~~
效能提升大概可提升6X
## 參考資料
* [RINC同學](https://embedded2016.hackpad.com/2016q1-Homework-2A-GalzL151aZc)(有call graph圖及multithread)[gprof2](https://github.com/jrfonseca/gprof2dot)
* [吳彥寬同學](https://embedded2016.hackpad.com/2016q1-Homework-2A-wOu40KzMaIP)(對於SIMD實做及multithread)
* [陳品睿同學](https://embedded2016.hackpad.com/2016q1-Homework-2A-bLGQtRraTES)(有實做各種 multithread)
* [POSIX pthreads Tutorial](http://randu.org/tutorials/threads/)
* [POSIX Threads Programming](https://computing.llnl.gov/tutorials/pthreads/)