# 2017q1 Homework1 (raytracing)
contributed by < `claaaaassic` >
### Reviewed by you74674
* commit內容蠻清楚的
* 關於loop unrolling,或許可以比較看看compiler的loop unrolling flag?
* 開發紀錄內容也很完整
## 開發環境
```shell
$ lscpu
Architecture: x86_64
CPU 作業模式: 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
每核心執行緒數:2
每通訊端核心數:2
Socket(s): 1
NUMA 節點: 1
供應商識別號: GenuineIntel
CPU 家族: 6
型號: 42
Model name: Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz
製程: 7
CPU MHz: 886.785
CPU max MHz: 2900.0000
CPU min MHz: 800.0000
BogoMIPS: 4589.49
虛擬: VT-x
L1d 快取: 32K
L1i 快取: 32K
L2 快取: 256K
L3 快取: 3072K
NUMA node0 CPU(s): 0-3
```
## 基本知識
gcc options
- `-O0` : 使用預設編譯模式(關閉最佳化)
- `-pg` : 編譯時加入一些 code 可讓 gprof 觀測
GNU gprof
- 分析什麼是程式執行中最花時間的,以及 function 之間的關係
call graph
- 追蹤 function 之間的關係
force inline
- inline是向編譯器提出“建議”,把inline的函數在函數位置直接展開(減輕系統負擔(overhead)),編譯器會對比兩者執行時間後選擇執行inline與否。
POSIX Thread
OpenMP
software pipelining
loop unrolling
## 作業要求
* 在 GitHub 上 fork [raytracing](https://github.com/sysprog21/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` 定義的若干數學操作函式很重要,可參考 [Investigating SSE cross product performance](http://threadlocalmutex.com/?p=8)、[MathFu](https://google.github.io/mathfu/) 原始程式碼,以及 [2015q3 Homework #1 Ext](http://wiki.csie.ncku.edu.tw/embedded/2015q3h1ext)
* 可善用 POSIX Thread, OpenMP, software pipelining, 以及 loop unrolling 一類的技巧來加速程式運作
* 將你的觀察、分析,以及各式效能改善過程,並善用 gnuplot 製圖,紀錄於「[作業區](https://hackmd.io/s/Hy2rieDYe)」
## graphviz
參考 [2016q3 hugikun999](https://hackmd.io/s/HyHhgcv6#) 的筆記
### 安裝過程
```shell
$sudo apt-get install python python-pip
$sudo pip install --upgrade pip
$sudo pip install gprof2dot
```
### 圖表
`$ gprof ./raytracing | gprof2dot | dot -T png -o output.png`
神奇的圖表就這樣跑出來了!非常有助於理解程式啊!原本醜醜的 call graph 只有一些文字表格,圖表真的好看很多,不過我這張圖是在已經使用 loop unrolling 跟 force inline 改善過了不是一開始的圖
![](https://i.imgur.com/CRLwzUK.png)
## 開發過程
首先看完 [video](https://www.youtube.com/watch?v=m1RmfOfSwno) "Homework 2" 的部份、實做程式的解說 [video](https://www.youtube.com/watch?v=V_rLyzecuaE)、[對應的共筆](https://embedded2016.hackpad.com/ep/pad/static/f5CCUGMQ4Kp)
這個作業要觀察 raytracing 的效能瓶頸,改寫檔案改善程式效能,過程中維持 `gcc -O0` (關閉最佳化),可以以 POSIX Thread, OpenMP, software pipelining, loop unrolling 為改善的方向。
### 以 make PROFILE=1 重新編譯程式碼,並且學習 gprof
在 Makefile 裡面找到這段程式
```shell
CFLAGS = \
-std=gnu99 -Wall -O0 -g
LDFLAGS = \
-lm
ifeq ($(strip $(PROFILE)),1)
PROF_FLAGS = -pg
CFLAGS += $(PROF_FLAGS)
LDFLAGS += $(PROF_FLAGS)
endif
```
也就是輸入 `$ make PROFILE=1` 可以編譯檔案後面帶著 `-pg` 的參數,查一下這個參數可以幹麻
> Generate extra code to write profile information suitable for the analysis program gprof
> [reference link](https://gcc.gnu.org/onlinedocs/gcc-6.3.0/gcc/Instrumentation-Options.html#Instrumentation-Options)
所以說輸入 `$ make PROFILE=1` 就可以開始來學習 gprof 了!
==注意:在 CFLAGS 及 LDFLAGS 都需要加上`-O0`==
首先觀察一下精美的 650 行程式碼
```shell
$ cloc *.[ch]
8 text files.
8 unique files.
0 files ignored.
http://cloc.sourceforge.net v 1.60 T=0.01 s (684.7 files/s, 73091.8 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
C 3 93 70 453
C/C++ Header 5 41 0 197
-------------------------------------------------------------------------------
SUM: 8 134 70 650
-------------------------------------------------------------------------------
```
接著看看執行時間
```shell
# Rendering scene
Done!
Execution time of raytracing() : 3.837321 sec
```
接著看看加上 `-pg` 的執行時間
```shell
# Rendering scene
Done!
Execution time of raytracing() : 7.437667 sec
```
居然要花到大約 7.43 秒,那就來看一下多花的這些執行時間給我的資訊吧
```shell
$ gporf ./raytracing | less
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
20.63 0.67 0.67 56956357 0.00 0.00 subtract_vector
20.01 1.32 0.65 69646433 0.00 0.00 dot_product
10.16 1.65 0.33 10598450 0.00 0.00 normalize
9.54 1.96 0.31 17836094 0.00 0.00 add_vector
7.70 2.21 0.25 13861875 0.00 0.00 rayRectangularIntersection
6.16 2.41 0.20 13861875 0.00 0.00 raySphereIntersection
6.00 2.61 0.20 31410180 0.00 0.00 multiply_vector
4.93 2.77 0.16 4620625 0.00 0.00 ray_hit_object
3.39 2.88 0.11 17821809 0.00 0.00 cross_product
2.62 2.96 0.09 4221152 0.00 0.00 multiply_vectors
2.46 3.04 0.08 1048576 0.00 0.00 ray_color
1.54 3.09 0.05 2110576 0.00 0.00 localColor
```
光是花在前三個 function 居然就佔了整體 50% 左右,呼叫的次數也是千萬等級的,如果減少前三個 function 的執行時間應該可以有效的增加效能
## Optimization : loop unrolling
針對 math-toolkit.h 裡面的迴圈進行改善,把三層的迴圈展開!雖然降低可讀性但是可以增快速度
執行時間由 **3.837321 sec** 降為 **2.617314 sec**,大約為原本的 **68.2%**
```
# Rendering scene
Done!
Execution time of raytracing() : 2.617314 sec
```
根據 gprof 產生的分析可以比較一下使用 loop unrolling 的函式所獲得的改善,觀察 self 欄位可以看出函式的總執行時間
subtract_vector 0.67 > 0.22
dot_product 0.65 > 0.32
add_vector 0.31 > 0.04
multiply_vectors 0.09 > 0.02
multiply_vector 0.20 > 0.19
時間上大都有所減少
```shell
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
17.50 0.32 0.32 69646433 0.00 0.00 dot_product
14.22 0.58 0.26 13861875 0.00 0.00 rayRectangularIntersection
14.22 0.84 0.26 10598450 0.00 0.00 normalize
11.76 1.06 0.22 56956357 0.00 0.00 subtract_vector
10.12 1.24 0.19 31410180 0.00 0.00 multiply_vector
7.93 1.39 0.15 17821809 0.00 0.00 cross_product
5.19 1.48 0.10 4620625 0.00 0.00 ray_hit_object
4.37 1.56 0.08 1048576 0.00 0.00 ray_color
2.73 1.61 0.05 13861875 0.00 0.00 raySphereIntersection
2.73 1.66 0.05 2110576 0.00 0.00 compute_specular_diffuse
2.19 1.70 0.04 17836094 0.00 0.00 add_vector
1.64 1.73 0.03 1241598 0.00 0.00 refraction
1.09 1.75 0.02 2110576 0.00 0.00 localColor
1.09 1.77 0.02 1048576 0.00 0.00 rayConstruction
0.82 1.79 0.02 4221152 0.00 0.00 multiply_vectors
```
從圖表上也可以看出整體的時間差異
![](https://i.imgur.com/QHHvQbt.png)
:::success
> [time=Mon, Mar 13, 2017 1:41 AM]
這邊看完文章[Modern Microprocessers](http://www.lighterra.com/papers/modernmicroprocessors/)之後,我不知道哪來的想法覺得使用 loop unrolling 應該可以減少 cache misses 以及使得 instructions per cycle 增加,以下進行 perf 比較
* original
```
Performance counter stats for './raytracing' (10 runs):
51,357 cache-misses # 26.809 % of all cache refs ( +- 2.60% )
191,564 cache-references ( +- 6.88% )
19,467,327,586 instructions # 1.82 insn per cycle ( +- 0.00% )
10,722,360,954 cycles ( +- 0.09% )
```
* loop unrolling
```
Performance counter stats for './raytracing' (10 runs):
45,413 cache-misses # 28.828 % of all cache refs ( +- 3.19% )
157,528 cache-references ( +- 3.48% )
12,315,238,559 instructions # 1.69 insn per cycle ( +- 0.00% )
7,285,757,575 cycles ( +- 0.07% )
```
結果卻是相反的, cache misses 以及 instructions per cycle 雖然總量減少,可是比例上來說卻是增加的,所以說使用 loop unrolling 理論上來說可以減少 branch prediction 產生的 cache misses ,但結果上來說卻是沒有符合理論。
:::
## Optimization : force inline
在 Makefile 中的 gcc 有使用 `-O0` 這個關閉最佳化的選項,但如果想要減少函數呼叫次數,可以使用 force inline,將 function 放入呼叫該 function 的位置,使用方法為加上 __attribute__((always_inline))
首先測試看看前三耗時的 function 使用 force inline 會不會變更快
```
# Rendering scene
Done!
Execution time of raytracing() : 2.488187 sec
```
減少了 0.129127 sec ,看來是可以降低執行時間,接著實驗把所有 math-toolkit.h 裡面的函式都 force inline
```
# Rendering scene
Done!
Execution time of raytracing() : 2.282104 sec
```
使用 loop unrolling + force inline 的結果與單獨使用 loop unrolling 比較
降低執行時間 **0.33512 sec** ,執行時間變為原本的 **87.2%**
從圖表上可以比較清楚看到做了 optimized 之後的時間差異
![](https://i.imgur.com/zCLkb1p.png)
從 gprof 上可以看出剛剛 math-toolkit.h 裡面的函式都已經被展開了
```shell
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds sec onds calls s/call s/call name
31.32 0.65 0.65 13861875 0.00 0.00 rayRectangularIntersection
18.70 1.03 0.39 13861875 0.00 0.00 raySphereIntersection
15.05 1.34 0.31 2110576 0.00 0.00 compute_specular_diffuse
9.23 1.53 0.19 2110576 0.00 0.00 localColor
8.26 1.70 0.17 1048576 0.00 0.00 ray_color
7.04 1.85 0.15 4620625 0.00 0.00 ray_hit_object
4.37 1.94 0.09 1048576 0.00 0.00 rayConstruction
1.94 1.98 0.04 1241598 0.00 0.00 refraction
1.46 2.01 0.03 1241598 0.00 0.00 reflection
0.97 2.03 0.02 2520791 0.00 0.00 idx_stack_top
0.49 2.04 0.01 1241598 0.00 0.00 protect_color_overflow
0.49 2.05 0.01 1204003 0.00 0.00 idx_stack_push
0.49 2.06 0.01 1048576 0.00 0.00 idx_stack_init
0.24 2.06 0.01 113297 0.00 0.00 fresnel
0.00 2.06 0.00 2558386 0.00 0.00 idx_stack_empty
0.00 2.06 0.00 37595 0.00 0.00 idx_stack_pop
```
## OpenMP
參考 [OpenMP简易教程(pdf) ](https://drive.google.com/file/d/0B4WZ6ihm_C7zYkdvNUJyYlJFRG8/view)
#pragma omp 指令 [子句[子句]...]
- parallel for 表示接下來的for 迴圈將被並行執行
- private() 可以把變數宣告爲私有變數,讓其他執行緒無法存取,可以解決race condition的問題
- lastprivate 可以把變數結束後繼續取用
首先先修改 Makefile,讓它可以執行 openMP
```
ifeq ($(strip $(OPENMP)),1)
PROF_FLAGS = -fopenmp
CFLAGS += $(PROF_FLAGS)
LDFLAGS += $(PROF_FLAGS)
endif
```
接著根據 call function 裡面看到 raytracing 是大部分程式的進入點,而且裡面大部分程式碼使用 for 迴圈,使用 openMP 應該會有一些改變
stk, d, object_color 在 raytracing 迴圈裡面的 ray_color 裡面有改變值
為了防止 race condition 把他們設為 private
在 for 的最外面加上這句
`#pragma omp parallel for private(stk, d, object_color)`
時間上從 2.282104 sec 降到了 1.114757 sec
```
# Rendering
Done!
Execution time of raytracing() : 1.114757 sec
Verified OK
```
openMP 裡面有個 schedule 語法可以用,有四種參數可以選擇,實際上三種,第四種是根據系統設定為前三種的其中一個
- static
- 如果沒有使用 schedule 預設會加上 schedule(static),他的方法是把所有的迴圈平均分給每一個子程序
- dynamic
- 這個是動態的分配
- guided
- 這個則是越前面接觸到的子程序處理越多迴圈,逐漸遞減
剛剛沒加 schedule 時就是 static 了
Execution time of raytracing() : 1.114757 sec
使用 dynamic
Execution time of raytracing() : 0.964898 sec
使用 guided
Execution time of raytracing() : 0.975975 sec
使用 dynamic 參數或 guided 相差不遠,不過都比沒加時快大約 0.05 sec
最後選擇使用 dynamic
## 參考資料
[B02: raytracing](https://hackmd.io/s/HyuBWDwYl#)
[Enhance raytracing program](https://embedded2016.hackpad.com/ep/pad/static/f5CCUGMQ4Kp)