owned this note
owned this note
Published
Linked with GitHub
# 2016q3 Homework1 (raytracing)
#### contributed by <`andy19950`>
### Review TotallyWrong
* Callgraph 是個很好用的Tool 可以嘗試一下,可以看到一些不一樣的程式Behavior。其實還有AVX,Software Pipline等方案可以嘗試。
### 使用gprof分析程式熱點
```
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
18.06 0.48 0.48 69646433 0.00 0.00 dot_product
16.55 0.92 0.44 56956357 0.00 0.00 subtract_vector
11.66 1.23 0.31 31410180 0.00 0.00 multiply_vector
8.46 1.46 0.23 17836094 0.00 0.00 add_vector
8.46 1.68 0.23 13861875 0.00 0.00 rayRectangularIntersection
```
- 我們擷取前五名最花時間的function來對它們做最佳化!!
- 目標是可以利用程式碼的最佳化達到跟編譯器最佳化一樣的效果
---
### 首先使用-Ofast來看看編譯器最佳化的結果
```
Performance counter stats for './raytracing' (100 runs):
62,894 cache-misses #27.256 % of all cache refs ( +- 1.64% )
230,751 cache-references ( +- 3.17% )
262,129,289 branch ( +- 0.00% )
3,417,337,773 instructions #1.79 insns per cycle ( +- 0.00% )
1,905,665,597 cycles ( +- 0.12% )
0.646348227 seconds time elapsed ( +- 0.40% )
```
- 平均執行時間 : 0.64 sec
- branch 次數 : 2.6億次
---
### 接下來對 math-toolkit.h 進行優化
- 對有 for迴圈 的 function 使用 loop unrolling
- 以 add_vector 為例
``` clike=
void add_vector(const double *a, const double *b, double *out)
{
out[0] = a[0] + b[0];
out[1] = a[1] + b[1];
out[2] = a[2] + b[2];
}
```
- 修改後的結果如下,可以發現優化過的 function 執行時間都有下降
```
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
16.19 0.28 0.28 69646433 0.00 0.00 dot_product
15.89 0.55 0.27 13861875 0.00 0.00 rayRectangularIntersection
13.83 0.78 0.24 56956357 0.00 0.00 subtract_vector
10.59 0.96 0.18 10598450 0.00 0.00 normalize
7.06 1.08 0.12 31410180 0.00 0.00 multiply_vector
```
- 使用 perf 觀察可以發現 branch 次數大幅下降!!
- 執行時間距離目標還很遠,必須找其他方法降低。
```
Performance counter stats for './raytracing' (100 runs):
106,186 cache-misses #24.499 % of all cache refs ( +- 2.82% )
433,426 cache-references ( +- 2.71% )
969,948,464 branch ( +- 0.00% )
12,314,790,448 instructions #1.89 insns per cycle ( +- 0.00% )
6,506,120,704 cycles ( +- 0.07% )
2.152467875 seconds time elapsed ( +- 0.14% )
```
---
### 再一次觀察 gprof
- 我們可以發現一個重點程式 : raytracing
- 它的呼叫次數只有一次但執行時間卻可以跟呼叫百萬次的一樣久
```
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
1.83 1.50 0.03 3838091 0.00 0.00 length
1.83 1.53 0.03 1 0.03 1.64 raytracing
1.22 1.55 0.02 2110576 0.00 0.00 localColor
```
---
### 觀察 raytracing()
- 我們可以知道他是最後將圖片計算出來的函式
- 程式碼的部份用三個for迴圈來計算每個 pixel 的RGB值
- 使用 loop unrolling 的話會讓程式碼太龐大而且不知道圖片的大小
- 所以這邊我使用 pthread 來平行處理每個 pixel 使其加速
:::info
*** 在優化 raytracing() 之前先來了解一下什麼是 pthread**
:::
---
### POSIX Threads
#### pthread_create() VS fork()
- 產生以及控管 thread 相較於 process 所需的系統資源低很多
- 下表為 fork 以及 pthread_create 在不同 cpu 執行50000個 process/threads 所使用的時間
```
platform | fork | pthread_create()
====================| real user sys | real user sys
Intel 2.6 GHz Xeon | 8.1 0.1 2.9 | 0.9 0.2 0.3
Intel 2.8 GHz Xeon | 4.4 0.4 4.3 | 0.7 0.2 0.5
AMD 2.3 GHz Opteron | 12.5 1.0 12.5 | 1.2 0.2 1.3
AMD 2.4 GHz Opteron | 17.6 2.2 15.7 | 1.4 0.3 1.3
```
#### Thread 各自獨立的單位
1. Stack pointer
2. Registers
3. Scheduling properties
4. Set of pending and blocked signals
5. Thread specific data.
#### Shared Memory
![](https://i.imgur.com/QuJYU2C.gif)
- 雖然每個 thread 都有自己獨立的 private data 以及大家一起使用的 shared memory
- 但因為每個 thread 都是同步執行的,也就是誰先誰後沒有一定順序,所以使用 pthread 來同步執行必須先確保程式碼沒有相依性。
---
### 優化 raytracing()
- 根據上面的結論,我們必須找出程式碼沒有相依性的地方。
- 觀察程式碼後可以發現前兩個 for 迴圈只是在指定每個 pixel 的位置
- 想法: 把圖片分成幾個區塊,用 pthread 平行運算
#### 在 main 的部份生成 pthread 同時呼叫raytracing
```clike=
for(j=0; j<THREAD_NUM; j++){
/*---這邊省略把原本raytracing的參數包成struct傳入thread,詳細請參考我的github---*/
input* box = (input*) malloc (sizeof(input));
rc = pthread_create(&tid[j], &attr, raytracing, (void*) &box) ;
}
for(j=0; j<THREAD_NUM; j++)
rc = pthread_join(tid[j], NULL);
```
- input box為我宣告的struct,成員有原本需要傳入raytracing的參數。
- 因為 pthread_create 只有第四個參數可以當作raytracing的傳入值,所以我把它包成結構傳入。
- 需要注意的是根據定義第四個參數的型態必須是 (void*)。
#### 在 raytracing的部份 把原本最外層的迴圈條件改變讓程式只跑整張圖的一部分
```clike=
for (int j = box->j; j < (box->j + box->height / THREAD_NUM); j++) {
for (int i = 0; i < box->width; i++) {
```
- 每個thread都把寬度算完高度的部份用 THREAD_NUM 為單位去分。
- 簡單來說就是把整張圖橫切等分為 THREAD_NUM個,每個用一個 thread 去計算它的pixel。
- 下面部份的code若有用到原本的參數要向上面一樣使用 box->variable。
#### 結果分析
- thread 不是越多越好,512個 thread 執行時間不是最短!!
- 最主要的原因應該是 main 還在分配 thread 的時候,可能就有**前面的 thread 已經跑完了**,這樣就沒有平行化的效果,卻還要付出呼叫 thread 的代價。
- 經過實驗發現 THREAD_NUM = 8 開始效能算是最低(數據如下:)
```
THREAD_NUM EXEC_TIME || THREAD_NUM EXEC_TIME
512 0.996285 sec || 256 1.016958 sec
128 1.003348 sec || 64 1.022087 sec
32 0.957354 sec || 16 0.965541 sec
8 0.968322 sec || 4 1.089226 sec
2 1.217084 sec || 1 2.160865 sec
```
#### 使用 inline attribute (8 threads)
```
Performance counter stats for './raytracing' (100 runs):
98,242 cache-misses #11.745 % of all cache refs ( +- 0.80% )
836,439 cache-references ( +- 0.80% )
545,838,551 branch ( +- 0.00% )
10,597,581,108 instructions #1.19 insns per cycle ( +- 0.00% )
8,893,931,480 cycles ( +- 0.13% )
0.860754705 seconds time elapsed ( +- 0.26% )
```
- 時間可以降至 0.86 sec
- ---
### 結論
- 一開始使用 -Ofast 時間為 0.64 sec -O0 時間為 3.02 sec
- 優化後使用 -Ofast 時間為 0.26 sec -O0 時間為 0.86 sec
- 程式碼的最佳化竟然可以讓執行時間縮減成原來的 1/4
- 希望未來能夠再把這份程式優化,讓我達成一開始的目標。
---
### Reference
* [pthread 傳遞多個參數](http://pccts.blogspot.tw/2007/11/pthreadcreate.html)
* [POSIX Threads Programing](https://computing.llnl.gov/tutorials/pthreads/)
* [fork vs pthread](https://computing.llnl.gov/tutorials/pthreads/fork_vs_thread.txt)
* [吳彥寬的共筆](https://embedded2016.hackpad.com/2016q1-Homework-2A-wOu40KzMaIP)
- 特別感謝上面那位同學的共筆,非常詳細的解說,尤其是他解決了我苦思已久的bug!!