# 2016q3 Homework1 (raytracing)
contributed by <`TempoJiJi`>
###### tags: `TempoJiJi` `raytracing` `sysprog21`
### Reviewed by `SarahYuHanCheng`
* 建議圖表數值命名可以用縮寫取代代稱
![](https://i.imgur.com/hxfB17o.png)
## 預期目標
之前已對raytracing做過各方面的研究,但會做一次總復習,且會着重在上次未能達到的目標上。
過去的共筆:[組別共筆](https://embedded2016.hackpad.com/ep/pad/static/f5CCUGMQ4Kp), [個人共筆](https://embedded2016.hackpad.com/ep/pad/static/s8ujtGxBML2)
* 學習 gprof
* 以 gprof 指出效能瓶頸,並且著手改寫檔案 math-toolkit.h 在內的函式實做
* 善用 POSIX Thread, OpenMP, software pipelining, 以及 loop unrolling 一類的技巧來加速程式運作
* 實做SIMD
## 開發環境
* Ubuntu 16.04 LTS
* L1d cache: 32K
* L1i cache: 32K
* L2 cache: 256K
* L3 cache: 3072K
* Architecture: x86_64
* CPU op-mode(s): 32-bit, 64-bit
* Byte Order: Little Endian
* CPU(s): 4
* Model name: Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz
---
測試未優化的程式效能:
```
# Rendering scene
Done!
Execution time of raytracing() : 3.338328 sec
```
利用gprof觀察程式效能及行爲,在Makefile的編譯選項裏加入 ==-pg== ,再進行編譯跟執行,就會產生一個gmon.out檔:
```clike
$ gprof raytracing gmon.out
% cumulative self self total
time seconds seconds calls s/call s/call name
24.91 0.60 0.60 69646433 0.00 0.00 dot_product
14.12 0.94 0.34 56956357 0.00 0.00 subtract_vector
10.38 1.19 0.25 13861875 0.00 0.00 rayRectangularIntersection
9.34 1.42 0.23 31410180 0.00 0.00 multiply_vector
8.72 1.63 0.21 10598450 0.00 0.00 normalize
7.06 1.80 0.17 4620625 0.00 0.00 ray_hit_object
4.98 1.92 0.12 17836094 0.00 0.00 add_vector
4.57 2.03 0.11 13861875 0.00 0.00 raySphereIntersection
4.15 2.13 0.10 17821809 0.00 0.00 cross_product
2.49 2.19 0.06 1048576 0.00 0.00 ray_color
2.08 2.24 0.05 1048576 0.00 0.00 rayConstruction
```
這裏可以看到dot_product跟subtrace_vector是最花時間的地方,程式執行期間總共被呼叫了接近7前萬次(dot_product)。
## 所以先從math-toolkit.h開始觀察:
### Loop Unrolling
優點:
* 分支預測(branch predictor)失敗減少。
* 如果循環體內語句沒有數據相關,增加了並發執行的機會。
* 可以在執行時動態循環展開。
缺點:
* 降低可讀性
```
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;
}
```
這裏可以看到利用迴圈來計算dot_product,而迴圈會有branch的情況發生,導致效能不好,所以就將迴圈拆開(loop unrolling):
```clike=
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() : 2.999242 sec
```
可以看到只改一個dot_product時間就快了近0.4秒左右。
---
### Force Inline
inline是向編譯器提出“建議”,把inline的函數在函數位置直接展開(減輕系統負擔(overhead)),編譯器會對比兩者執行時間後選擇執行inline與否。
優點:
* 減少function call
在fucntion後面加上 __forceinline
```clike=
static inline __forceinline
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;
}
```
編譯加上 -D__forceinline="__attribute__((always_inline))" 進行編譯:
```
# Rendering scene
Done!
Execution time of raytracing() : 3.059393 sec
```
時間比原本的快了0.2秒左右,接下來用gprof觀察function call:
```
% cumulative self self total
time seconds seconds calls s/call s/call name
34.98 1.00 1.00 13861875 0.00 0.00 rayRectangularIntersection
22.38 1.64 0.64 13861875 0.00 0.00 raySphereIntersection
10.84 1.95 0.31 2110576 0.00 0.00 compute_specular_diffuse
9.44 2.22 0.27 2110576 0.00 0.00 localColor
7.17 2.43 0.21 1048576 0.00 0.00 ray_color
5.42 2.58 0.16 4620625 0.00 0.00 ray_hit_object
4.20 2.70 0.12 1048576 0.00 0.00 rayConstruction
```
可以看到math_tookit.h裏的function已經不見了
---
### Macro
優點:
* 執行速度快,沒有堆疊的 push 和 pop 動作的需要,減少時間的耗損
缺點:
* 巨集被呼叫多次以後,會耗損存放及使用大量的記憶體空間
將math_tookit.h裏的function改爲Macro來進行計算,這樣就能減少function call(stack frame的push,pop等行爲)所帶來的時間花費
```clike=
#define DOT(a,b) ((a[0]*b[0])+(a[1]*b[1])+(a[2]*b[2]))
```
```
# Rendering scene
Done!
Execution time of raytracing() : 2.741537 sec
```
這裏可以看到時間比loop unrolling快了近0.2秒左右,因此決定將math_tookit.h裏的花費較大的function都改爲Macro argument
根據gprof的結果,可以知道哪個function的花費最大,因此將它們都改爲Macro argument即可
```clike
#define DOT(a,b) ((a[0]*b[0])+(a[1]*b[1])+(a[2]*b[2]))
#define SUB_VEC(a,b,c) (c[0]=a[0]-b[0], c[1]=a[1]-b[1], c[2]=a[2]-b[2])
#define MVEC(a,b,out) (out[0]=a[0]*(b), out[1]=a[1]*(b), out[2]=a[2]*(b))
#define ADD(a,b,c) (c[0]=a[0]+b[0], c[1]=a[1]+b[1], c[2]=a[2]+b[2])
#define CDOT(a,b,out) (out[0]=(a[1]*b[2])-(a[2]*b[1]),out[1]=(a[2]*b[0])-(a[0]*b[2]),out[2]=(a[0]*b[1])-(a[1]*b[0]))
```
```
# Rendering scene
Done!
Execution time of raytracing() : 1.634365 sec
```
可以看到只改了math_tookit.h整個程式的效能就快了接近1倍,可見程式的效能瓶頸真的是在math_tookit.h裏。
---
## 實做PThread 跟 OpenMP
### OpenMP
首先來實做OpenMP,在for迴圈上加上:
```
#pragma omp parallel for num_threads(4) \
private(x), private(y)
```
num_threads是要建立的執行緒數量。private可以確保變數在各個執行緒裏是獨立的,並不會因爲某個執行緒改變了變數,而影響到其它執行緒。
在raytracing的兩層for迴圈上加上:
```
#pragma omp parallel for num_threads(4) \
private(stk), private(d), \
private(object_color)
for (int j = 0 ; j < 512; j++) {
for (int i = 0; i < 512; i++) {
double r = 0, g = 0, b = 0;
/* MSAA */
```
這裏將stk、d、object_color設爲private,是因爲這3個的值是會在各個執行緒裏進行更動。
最後要記得 ==#include<omp.h>== , 以及在編譯選項中加上 ==-fopenp==
執行結果:
```
num_thread(4)
# Rendering scene
Done!
Execution time of raytracing() : 1.738263 sec
```
不同的num_thread比較圖:
![](https://i.imgur.com/D3ByhIL.png)
---
### POSIX Thread
當function的參數超過一個,在建立pthread的時候需要將所有參數打包起來,才能傳送給function:
```clike=
typedef struct __ARG {
uint8_t *pixels;
color background;
rectangular_node rectangulars;
sphere_node spheres;
light_node lights;
const viewpoint *View;
int start_j;
point3 u, v, w, d;
} arg;
```
將圖片分成不同的等份交給不同的執行緒去進行,這裏我會先將row分成4個等份,再用4個執行緒去執行:
```clike=
/* (*data).start_j 爲每個執行緒所執行的起點 */
for (int j = (*data).start_j ; j < 512; j+=MAXX) {
for (int i = 0; i < 512; i++) {
double r = 0, g = 0, b = 0;
/* MSAA */
...
```
執行結果:
```
# Rendering scene
Done!
Execution time of raytracing() : 1.603646 sec
```
不同thread數量的比較圖:
![](https://i.imgur.com/igoghWg.png)
由上圖可得知,thread的數量越多,不代表程式效能越好。
---
## 最佳優化,合並所有方法
合並所有方法,再跟不同的編譯優化做對比:
![](https://i.imgur.com/enu7J8N.png)