# 2017q1 Homework1 (raytracing)
contributed by <`vtim9907`>
### Reviewed by `petermouse`
* 用 gdb 來驗證 `register` 關鍵字仍是個很好的嘗試!
* 可以考慮使用 Pthread 或 OpenMP 將程式平行處理
* 可以對 SIMD 比原版慢的結果進一步提出解釋並驗證
* 建議使用圖表來呈現改善的成果
# 開發環境
- OS : Ubuntu 16.04.2 LTS
- Kernel Version : 4.8.0-36-generic
- Memory : 32 GB
- CPU : Intel® Core™ i5-6600 Processor
- cache :
- L1i cache : 4*32 KB
- L1d cache : 4*32 KB
- L2 cache : 4*256 KB
- L3 cache : 6144 KB
- L1 cache imformation ( sudo dmidecode -t cache ) :
```
Handle 0x001E, DMI type 7, 19 bytes
Cache Information
Socket Designation: L1 Cache
Configuration: Enabled, Not Socketed, Level 1
Operational Mode: Write Back
Location: Internal
Installed Size: 256 kB
Maximum Size: 256 kB
Supported SRAM Types:
Synchronous
Installed SRAM Type: Synchronous
Speed: Unknown
Error Correction Type: Parity
System Type: Unified
Associativity: 8-way Set-associative
```
# 優化前分析
## gprof
首先執行看看,得到結果 :
```
Execution time of raytracing() : 2.106784 sec
```
在開始優化之前,先做分析,找出該優化的地方,首先用 gprof 分析看看:
```
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
22.18 0.43 0.43 56956357 0.00 0.00 subtract_vector
21.15 0.84 0.41 69646433 0.00 0.00 dot_product
10.32 1.04 0.20 13861875 0.00 0.00 rayRectangularIntersection
8.51 1.21 0.17 31410180 0.00 0.00 multiply_vector
7.74 1.36 0.15 13861875 0.00 0.00 raySphereIntersection
7.22 1.50 0.14 10598450 0.00 0.00 normalize
5.67 1.61 0.11 17836094 0.00 0.00 add_vector
3.61 1.68 0.07 17821809 0.00 0.00 cross_product
3.09 1.74 0.06 4620625 0.00 0.00 ray_hit_object
2.32 1.78 0.05 4221152 0.00 0.00 multiply_vectors
1.55 1.81 0.03 2110576 0.00 0.00 localColor
1.55 1.84 0.03 1048576 0.00 0.00 ray_color
1.03 1.86 0.02 3838091 0.00 0.00 length
1.03 1.88 0.02 2110576 0.00 0.00 compute_specular_diffuse
1.03 1.90 0.02 1 0.02 1.94 raytracing
0.52 1.91 0.01 2520791 0.00 0.00 idx_stack_top
0.52 1.92 0.01 1204003 0.00 0.00 idx_stack_push
0.52 1.93 0.01 1048576 0.00 0.00 rayConstruction
0.52 1.94 0.01 1 0.01 0.01 calculateBasisVectors
0.00 1.94 0.00 2558386 0.00 0.00 idx_stack_empty
0.00 1.94 0.00 1241598 0.00 0.00 protect_color_overflow
0.00 1.94 0.00 1241598 0.00 0.00 reflection
0.00 1.94 0.00 1241598 0.00 0.00 refraction
0.00 1.94 0.00 1048576 0.00 0.00 idx_stack_init
0.00 1.94 0.00 113297 0.00 0.00 fresnel
0.00 1.94 0.00 37595 0.00 0.00 idx_stack_pop
```
省略了一些次數不多、耗時也不多的 function。可以看到前幾名被呼叫到的次數都是幾千萬次,該針對哪裡著手做優化,已經很明顯有了個大概的方向。
## gprof2dot
雖然用 gprof 可以得到以文字所呈現的 call graph,但是我只看了一下就覺得很累@@ 大概是我對這個程式還不是非常熟悉,每個 function 的名稱有時看過就忘了,所以我還是覺得要把 call graph 視覺化出來。
上網 google 了 "gprof call graph visualization",出現的前好幾筆資料都是建議使用 gprof2dot 這個工具,所以我也決定用用看效果如何。
安裝完相關套件後,執行:
```
gprof ./raytracing | gprof2dot | dot -Tpng -o output.png
```
產生了一個 .png 的圖檔!
![](https://i.imgur.com/YaNAHuM.png)
產生的圖檔其實滿大一張的,只是丟上來後變的有點小@@
憑借這張圖,就能比較輕鬆知道 function 之間的關係以及程式流向,對分析也有幫助!
# 優化
## Method 1 : loop unrolling
由於 loop 或是 if-else 相關類型的指令,都包含了 compare 以及 jump 的動作,雖然在寫高階語言時用這些方法比較方便,甚至因為省下了一些 code ,會有執行更快速的錯覺,但從彙編語言的角度來看,這些 branch 指令,每一步都是花費。
以現在這個例子來看,`math-toolkit.h` 裡,一共有 5 個 function 使用了 for loop ,而且其中就有 4 個是跑了千萬次以上的 function,合計共 180070216 次,再乘上 4 ,就有高達七億兩千萬個 branch 指令被執行過,如果我們輕鬆的將這些少少的 loop 展開,就可以省下大量 cycle!
單純針對 `math-toolkit.h` 裡的 function 做完 loop unrolling 的動作之後,實際測試結果:
```
Execution time of raytracing() : 1.468591 sec
```
其中前幾大耗時的 function 包含了被修改的五個 function 的資訊:
```
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
18.64 0.19 0.19 56956357 0.00 0.00 subtract_vector
13.73 0.33 0.14 69646433 0.00 0.00 dot_product
11.77 0.45 0.12 13861875 0.00 0.00 rayRectangularIntersection
7.85 0.53 0.08 17821809 0.00 0.00 cross_product
6.87 0.60 0.07 31410180 0.00 0.00 multiply_vector
6.87 0.67 0.07 13861875 0.00 0.00 raySphereIntersection
5.89 0.73 0.06 17836094 0.00 0.00 add_vector
5.89 0.79 0.06 4620625 0.00 0.00 ray_hit_object
3.92 0.83 0.04 10598450 0.00 0.00 normalize
3.92 0.87 0.04 1048576 0.00 0.00 ray_color
3.92 0.91 0.04 1 0.04 1.02 raytracing
3.43 0.95 0.04 4221152 0.00 0.00 multiply_vectors
```
總執行的時間減少了約 0.64 秒,從上面的資訊也很容易看得出來,因為主要運算的 function 加快了,所以某些 function 也多少加快了一些;雖然只是做了簡單的優話,卻產生了極大的成效!這也多虧了 gprof 的分析,才得以輕易的看出哪些 function 是需要被修改的重點。
### loop unrolling + always_inline
之後在看 [C语言inline详细讲解](http://www.cnblogs.com/cnmaizi/archive/2011/01/19/1939686.html) 時,赫然發現原來 `inline` 是個 "建議" 型關鍵字,compiler 並不一定會因為有加 inline 就保證會對該 function 做內聯擴展,也就是說我之前寫的 code 也許根本沒起到什麼作用@@ 不過還好文章後面馬上提到加上 `__attribute__ ((always_inline))` 就可以強制 complier 將該 function 視為 inline function ,因此重新修改過後,再次編譯執行:
```
Performance counter stats for './raytracing' (10 runs):
118,2873 cache-misses # 63.538 % of all cache refs ( +- 27.88% )
186,1665 cache-references ( +- 23.03% )
240,9906 L1-dcache-load-misses ( +- 3.27% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
99,5265 L1-icache-load-misses ( +- 18.47% )
1.386430395 seconds time elapsed ( +- 2.20% )
```
結果又縮短了 0.11 秒左右!效能比優化前提昇了 36% !
## Method 2 : SIMD
在開始之前,不確定我的 cpu 有支援哪些東西可以讓我用,於是去看了 [spec](http://www.cpu-world.com/CPUs/Core_i5/Intel-Core%20i5-6600.html) ,發現 feature 裡有提到支援 AVX/AVX2,於是想從指令集裡找適合的指令為 function 做優化,以前面 loop unrolling 為基底,希望能夠幫助效能更加提昇。
一開始我用了 AVX/AVX2 intrinsics 把 `math-toolkit.h` 裡幾乎全部的 function 全部都改寫過,不過與預期的不同,當我全部改寫完重新執行後:
- 未強制 inlinie 版
```
Execution time of raytracing() : 3.650931 sec
```
- 強制 inline 版
```
Execution time of raytracing() : 3.272730 sec
```
就算經過強制 inline 後,也比未優化版本還慢了 0.75 秒,相較於 loop unrolling 的版本,差勁了更多。
### AVX + always_inline + register
我在觀賞學長姐得共筆時,看到有人在 code 裡加上了 "register" 的關鍵字,這是個好像有聽過,可是我卻從來沒用過得東西,特別去看了一下 register 有什麼作用,馬上看到 register 跟 inline 一樣,只是個建議型關鍵字..., 而被標示 register 的變數 "有機會" 會被放到 cpu 的寄存器裡面,大幅提昇 cpu <>訪問</s> 存取的速度!聽起來似乎很適合加在跑了千萬次的 function 裡的變數! 於是我就加上了之後編譯執行看看:
:::danger
access 的繁體中文翻譯是「存取」,不是「訪問」,請留意 --jserv
:::
```
Execution time of raytracing() : 2.842533 sec
```
效能提昇了不少,比未加上 register 的版本又快了 0.4 秒,但可惜的是,還是慢了 loop unrolling 的版本許多。
然而我還是想知道,善用了 cpu register 之後,cache 會發生什麼樣的變化,於是我用 perf 跑了加 register 之前與之後的版本,來觀察 cache-miss 發生的情況:
- 沒使用 register 關鍵字
```
Execution time of raytracing() : 3.376952 sec
Performance counter stats for './raytracing' (10 runs):
428,9820 cache-misses # 71.062 % of all cache refs ( +- 26.18% )
603,6756 cache-references ( +- 21.22% )
814,9161 L1-dcache-load-misses ( +- 3.59% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
2,9194,2937 L1-icache-load-misses ( +- 0.58% )
3.575088791 seconds time elapsed ( +- 2.74% )
```
- 有用 register 關鍵字
```
Execution time of raytracing() : 2.816457 sec
Performance counter stats for './raytracing' (10 runs):
47,6384 cache-misses # 31.484 % of all cache refs ( +- 4.11% )
151,3082 cache-references ( +- 10.23% )
435,9741 L1-dcache-load-misses ( +- 1.71% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
2,0195,5565 L1-icache-load-misses ( +- 0.06% )
2.813734510 seconds time elapsed ( +- 0.11% )
```
加了 register 關鍵字後,cache-miss 大幅降低! 從四百多萬變成四十幾萬!並且 cache-miss 發生的機率也大幅下降了 40%; L1-icache-load-misses 也少了一億次左右!
cache reference 也減少了許多,我想應該有許多地方由 cpu register 代勞的關係。
### loop unrolling + always_inline + register
同樣的,我也想為前面 loop unrolling 的版本加上 register 試試:
```
Performance counter stats for './raytracing' (10 runs):
24,6774 cache-misses # 42.308 % of all cache refs ( +- 3.21% )
58,3278 cache-references ( +- 10.85% )
209,1464 L1-dcache-load-misses ( +- 1.38% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
42,2837 L1-icache-load-misses ( +- 5.41% )
1.264693515 seconds time elapsed ( +- 0.22% )
```
果然相較原來 loop unrolling + 強制 inline 的版本,快了 0.12 秒, cache-miss 也變得更低。
### register 驗證測試
不過這時我突然想起,既然 register 也是一個建議型的 keyword 那我怎麼能確定我加進 code 裡的 register 是否真的有起到作用呢? 於是我 google 大略看了一下 register 有沒有類似 always inline 的用法,不過似乎沒有,~~而且大多數人似乎都覺得 register 是一個沒有用的關鍵字了,大家都只用 complier 做優化 QQ~~
> 更精確的來說,在 C/C++ 規格中已經標注 "[Remove Deprecated Use of the register Keyword](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4340.html)",內容說明 register 關鍵字的功能已經從 Deprecated ,轉為移除該關鍵字的功能,但仍然保留了這個關鍵字,以做日後使用。
>> 感謝老師提醒,這邊不應該看別人的評論妄下結論。日後若對程式碼有疑惑,我會先看 spec 的!
不過我突然想到既然我都已經強制 inline 了,那我指名要放進 register 的變數應該也真的都有照我想的做了吧!? 為了確定這樣的想法是否正確,我想找個辦法觀察 cpu register 的運作,一方面確定強制 inline 是否也順便強制 register,一方面了解效能提昇的原因;我選擇用 gdb 來做接下來的測試。
### gdb
#### 測試一
我想先做個簡單的測試來確認 register 是否真的有運作? 強制 inline 真的也能強制 register 嘛?
首先我把 dot_product() 改成 :
```clike=
static inline __attribute__ ((always_inline))
double dot_product(const double *v1, const double *v2)
{
__m256d p1 = _mm256_set_pd(v1[0], v1[1], v1[2], 0.0);
__m256d p2 = _mm256_set_pd(v2[0], v2[1], v2[2], 0.0);
register __m256d mul = _mm256_mul_pd(p1, p2);
register __m256d hadd = _mm256_hadd_pd(mul, mul);
register __m128d result1 = _mm_set1_pd(hadd[3]);
register __m128d result2 = _mm_set1_pd(hadd[1]);
return _mm_add_pd(result1, result2)[1];
}
```
p1, p2 是沒有加 register 的變數,而其他如 mul, hadd 都有,主要是希望透過 gdb 來看這些變數是否有被經過優化,有加 register 的地方照理來說應該是被優化過的,其他地方因為把 complier 的優化關掉了,所以都沒有優化過。
```
vtim@vtim-ubuntu:~/raytracing$ make
cc -std=gnu99 -Wall -O0 -g -mavx2 -g3 -c -o objects.o objects.c
cc -std=gnu99 -Wall -O0 -g -mavx2 -g3 -c -o raytracing.o raytracing.c
cc -std=gnu99 -Wall -O0 -g -mavx2 -g3 -c -o main.o main.c
cc -o raytracing objects.o raytracing.o main.o -lm
vtim@vtim-ubuntu:~/raytracing$ gdb -q raytracing // 用 gdb 跑 raytracing
Reading symbols from raytracing...done.
(gdb) l dot_product
99 }
100
101 static inline __attribute__ ((always_inline))
102 double dot_product(const double *v1, const double *v2)
103 {
104 __m256d p1 = _mm256_set_pd(v1[0], v1[1], v1[2], 0.0);
105 __m256d p2 = _mm256_set_pd(v2[0], v2[1], v2[2], 0.0);
106 register __m256d mul = _mm256_mul_pd(p1, p2);
107 register __m256d hadd = _mm256_hadd_pd(mul, mul);
108 register __m128d result1 = _mm_set1_pd(hadd[3]);
(gdb) b 107 // 設斷點在上面 107 行的位置
Breakpoint 1 at 0x40163d: /home/vtim/raytracing/math-toolkit.h:107. (21 locations)
(gdb) r
Starting program: /home/vtim/raytracing/raytracing
# Rendering scene
Breakpoint 1, dot_product (v2=0x7fffffffcba0, v1=0x7fffffffcb60) at math-toolkit.h:107
107 register __m256d hadd = _mm256_hadd_pd(mul, mul);
(gdb) p p1
$1 = {0, 20, 0, 0} // 變數 p1 正常顯示
(gdb) p p2
$2 = {0, 17.345909496181164, -5.2263415432330067, 0} // 變數 p2 正常顯示
(gdb) p mul
$3 = <optimized out> // 灌上 register keyword 的變數 mul 顯示 <optimized out>
```
從上面最後幾行就看得到,有加 register 的部份的確有被 complier 採納,我做了好幾個測試結果都吻合前面的想法,所以我想強制 inline 確實也可強制 complier 採納 programmer 加入的 register keyword。
#### 測試二
>待續
### 將部份 function 改回 loop unrolling
由於這個優化版本似乎沒有什麼進展,所以我決定改變策略,只選擇部份運算量較大、被呼叫次數較多的 function,用 AVX 去優化,看會不會突破 loop unrolling 版本的效率。
可惜的是,當我逐個 function 去拔掉 AVX intrinsic instructions 時,效能就慢慢回升,直到全部拔掉,效能達到最高點,但是這就跟只用 loop unrolling 一模一樣阿!!!這真是令人太難過了@@ 我以為能夠搭配前面優話的版本進而更上一層樓,不過失敗了,我想應該有哪裡可以改進,我再思考看看。
## Method 3 : OpenCL
OpenCL 是一種針對異質性計算裝置進行平行化運算處理的 API 及 language,目前最常做的應用似乎是 GPGPU ,也就是可以運用顯示晶片做一些 3D 繪圖處理以外的運算,感覺是一種把資源的利用盡量提升到最大化的技術,決定試試看用 OpenCL 對 raytracing 做優化。
由於我的筆電沒有獨顯,所以只有 Intel cpu 內建的顯示晶片,在第一關架設環境時就遇到了麻煩...,由於這方面可以查到的資訊比較少,我只能照著 Intel 官方提供的一些說明照著安裝,但是一直處在不確定有沒有安裝完全,而到了準備 build 一個 OpenCL kernel 時,它卻是用一個 OpenCL™ Code Builder 的工具,讓我目前還有點混亂...待研究中。
# Reference
- Github [jrfonseca/gprof2dot](https://github.com/jrfonseca/gprof2dot)
- [Crunching Numbers with AVX and AVX2](https://www.codeproject.com/Articles/874396/Crunching-Numbers-with-AVX-and-AVX)
- [Overview: Intrinsics for Intel® Advanced Vector Extensions Instructions](http://technion.ac.il/doc/intel/compiler_c/main_cls/index.htm#intref_cls/common/intref_avx_extractf128_pd.htm)
- [C语言inline详细讲解](http://www.cnblogs.com/cnmaizi/archive/2011/01/19/1939686.html)