# 2016q3 Homework1 (compute-pi)
contributed by <`TempoJiJi`>
### Reviewed by `ierosodin`
* 解釋為何在兩種計算π的實驗中, openmp p4的抖動幅度都是最大的
* 列出不同function之間, 計算結果與π實際值的誤差圖(理論應相同, 但實際並非)
* 在使用Leibniz formula計算π時, (-1)^i^ 只有兩種結果, 不須計算pow(-1, i)
* 可以比較多種thread數量對效能的影響
* avx或avx_unroll的實作, code在N的計數上是有殘缺
* openmp在不同thread對效能的影響, 會是在real time上有較明顯的差異, 可以比較多組資料後進行分析
* 對於資料的抖動問題, 可以嘗試使用95%信賴區間對資料進行分類
## 預期目標
* 學習透過離散微積分求圓周率
* Leibniz formula for π
* 積分計算圓周率π
* Function
* 著手透過 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
>> 應該一併列出 processor model,這對解讀數據來說很重要 [name=jserv]
---
## 測試程式效能
觀察程式的行爲後,在Makefile裏看見了這個編譯選項:
` -DBASELINE `
才學到原來能在編譯動態使用#define
```shell
$ make check
time ./time_test_baseline
N = 400000000 , pi = 3.141593
4.41user 0.00system 0:04.43elapsed 99%CPU (0avgtext+0avgdata 1792maxresident)k
0inputs+0outputs (0major+87minor)pagefaults 0swaps
time ./time_test_openmp_2
N = 400000000 , pi = 3.141593
4.89user 0.00system 0:02.45elapsed 199%CPU (0avgtext+0avgdata 1784maxresident)k
0inputs+0outputs (0major+88minor)pagefaults 0swaps
time ./time_test_openmp_4
N = 400000000 , pi = 3.141593
9.49user 0.00system 0:02.46elapsed 385%CPU (0avgtext+0avgdata 1736maxresident)k
0inputs+0outputs (0major+90minor)pagefaults 0swaps
time ./time_test_avx
N = 400000000 , pi = 3.141593
1.28user 0.00system 0:01.28elapsed 99%CPU (0avgtext+0avgdata 1704maxresident)k
0inputs+0outputs (0major+83minor)pagefaults 0swaps
time ./time_test_avxunroll
N = 400000000 , pi = 3.141593
1.23user 0.00system 0:01.23elapsed 99%CPU (0avgtext+0avgdata 1716maxresident)k
0inputs+0outputs (0major+84minor)pagefaults 0swaps
```
在這裏就可以很清楚的看到每種不同的版本的執行時間(N=400000000),這裏有3種不同的時間:
* user time: 表示程式佔用CPU的總時間(在user mode下),其它的processes不算進去。如果當兩個執行在兩個CPU上運行各1秒,那麼總user time會是2秒
* system time: 表示程式在kernel mode下佔用CPU的總時間, 其它的processes不算進去
* real time: 在上面爲elapsed。也就是 Wall-clock time,從程式開始到結束的總時間,如果有其它程式在運行着,會影響到這個時間。
## CPU bound and I/O bound
* CPU bound: 指的是程序需要大量的 ==CPU computation== (對 CPU time 需求量大),相比之下僅需少量的 I/O operation,==效能取決於 CPU 速度==。
* I/O bound: ==需要大量 I/O operation== (I/O request 的頻率很高或一次 I/O request 成本很高),但僅需少量的 CPU computation,==效能取決於 I/O device 速度==。
1. `real < user` : 表示程式是==CPU bound==,所以使用平行處理對效能提升有幫助
2. `real ≒ user` : 表示程式是==CPU bound==,但即使使用平行處理,效能也不會有明顯的提升,有可能是計算兩沒有很大,又或是程式的相依性太高
3. `real > user` : 表示程式是==I/O bound==,成本大部分爲I/O操作,所以使用平行處理並不會帶來顯著的效能提昇 (或沒有提昇)
參考資料:
* [What do 'real', 'user' and 'sys' mean](http://stackoverflow.com/questions/556405/what-do-real-user-and-sys-mean-in-the-output-of-time1)
---
接下來看看make gencsv:
```shell=
$ make gencsv
for i in `seq 100 5000 25000`; do \
printf "%d," $i;\
./benchmark_clock_gettime $i; \
done > result_clock_gettime.csv
```
這裏的for相等於:
```clike=
for(i = 100; i <= 25000; i+=5000)
```
大意就是將N初始化爲100進行測試,每次加5000直到25000。然後將結果輸出到result_clock_gettime.csv裏
執行`make plot`輸出結果,在Makefile裏加入:
```ruby=
plot: gencsv
gnuplot runtime.gp
clean:
rm -f $(EXECUTABLE) *.o *.s result_clock_gettime.csv runtime.png
```
執行
```shell=
$ make plot
```
![](https://i.imgur.com/73L9XGg.png)
baseline的計算結果誤差圖:
![](https://i.imgur.com/tnWAYy8.png)
---
## 運用不同的方式計算pi
![](https://i.imgur.com/3mUF7g8.png)
上面這種方法就是目前baseline使用的
```clike=
double compute_pi_baseline(size_t N)
{
double pi = 0.0;
double dt = 1.0 / N; // dt = (b-a)/N, b = 1, a = 0
for (size_t i = 0; i < N; i++) {
double x = (double) i / N; // x = ti = a+(b-a)*i/N = i/N
pi += dt / (1.0 + x * x); // integrate 1/(1+x^2), i = 0....N
}
return pi * 4.0;
}
```
N 爲積分區間[0,1]的分段大小,理論上N分的越細(N值很大)精準度就越高,相對的計算量也很大
---
## Leibniz formula for π
![](https://i.imgur.com/Do0jj21.png)
參考資料:[Wikipedia](https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80)
Code:
```clike=
double compute_pi_leibniz(size_t N)
{
double pi = 0.0;
for(int i=0;i<N;i++)
pi += pow(-1,i) / (2*i+1);
return pi * 4.0;
}
```
leibniz_avx :
```clike=
double leibniz_avx(size_t N)
{
double pi = 0.0;
register __m256d ymm0,ymm1,ymm2,ymm3,ymm4;
ymm0 = _mm256_set_pd(1.0,-1.0,1.0,-1.0);
ymm1 = _mm256_set1_pd(1.0);
ymm2 = _mm256_set1_pd(2.0);
ymm4 = _mm256_setzero_pd();
for(int i=0 ; i <= N-4 ; i+=4){
ymm3 = _mm256_set_pd(i, i+1.0, i+2.0, i+3.0); // i to i+4
ymm3 = _mm256_mul_pd(ymm3 , ymm2); // 2*i (i to i+4)
ymm3 = _mm256_add_pd(ymm3 , ymm1); // 2*i+1 (i to i+4)
ymm3 = _mm256_div_pd(ymm0 , ymm3); // pow(-1,i) / ymm3
ymm4 = _mm256_add_pd(ymm4 , ymm3); // pi += ymm3
}
double tmp[4] __attribute__((aligned(32)));
_mm256_store_pd(tmp, ymm4); // move packed float64 values to 256-bit aligned memory location
pi += tmp[0] + tmp[1] + tmp[2] + tmp[3];
return pi * 4.0;
}
```
查看效能:
```shell=
$ make check
N = 400000000 , pi = 3.141593
2.20user 0.00system 0:02.20elapsed 99%CPU (0avgtext+0avgdata 1708maxresident)k
0inputs+0outputs (0major+83minor)pagefaults 0swaps
time ./time_test_leibniz_openmp_2
N = 400000000 , pi = 3.141593
2.45user 0.00system 0:01.23elapsed 199%CPU (0avgtext+0avgdata 1768maxresident)k
0inputs+0outputs (0major+86minor)pagefaults 0swaps
time ./time_test_leibniz_openmp_4
N = 400000000 , pi = 3.141593
4.78user 0.00system 0:01.23elapsed 386%CPU (0avgtext+0avgdata 1788maxresident)k
0inputs+0outputs (0major+93minor)pagefaults 0swaps
time ./time_test_leibniz_avx
N = 400000000 , pi = 3.141593
1.35user 0.00system 0:01.36elapsed 99%CPU (0avgtext+0avgdata 1788maxresident)k
0inputs+0outputs (0major+85minor)pagefaults 0swaps
time ./time_test_leibniz_avxunroll
N = 400000000 , pi = 3.141593
1.71user 0.00system 0:01.71elapsed 99%CPU (0avgtext+0avgdata 1932maxresident)k
0inputs+0outputs (0major+89minor)pagefaults 0swaps
```
在這裏觀察openmp:
* openmp_2 跟 openmp_4 的real time幾乎相等,在user time上卻相差很多,可見程式本身的相依性高。但跟普通的比起來,效能卻是提升了不少
以下爲leibniz各個不同實做的效能對比圖:
![](https://i.imgur.com/oNa580M.png)
leibniz的計算結果誤差圖:
![](https://i.imgur.com/UfN4Nyj.png)
---
## 思考如何精準計算時間
1. [clock()](https://linux.die.net/man/3/clock)
原型:
```clike
clock_t clock(void);
```
* clock() 函数的返回值类型是clock_t,它除以CLOCKS_PER_SEC来得出时间
* clock()有3個問題:
* 超過一小時,會overflow
* 沒有考慮CPU被子進程使用的情況
* 也不能區分user mode和kernel mode
clock()測試:
```clike=
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main( void )
{
long i=1000L;
clock_t start, finish;
double duration;
printf( "Time to do %ld empty loops is ", i );
start = clock();
while (--i){
system("cd");
}
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf( "%f seconds\n", duration );
return 0;
}
```
執行結果:
```shell=
$ time ./clock_test
Time to do 1000 empty loops is 0.049815 seconds
real 0m0.482s
user 0m0.000s
sys 0m0.052s
```
在這裏可以看到程式使用system("cd")這個system call,結果clock()所計算出來的時間跟real time卻差很多。
---
2. [clock_gettime()](https://linux.die.net/man/3/clock_gettime)
函式原型:
```clike
int clock_gettime(clockid_t clk_id, struct timespec *tp);
```
第一個參數clockid_t 確定要用哪個是種類型:
* `CLOCK_REALTIME`: 標準POSIX即時時鐘,利用變數xtime記錄RTC的時間。會隨着系統即時時間而改變,即從 `1970/1/1 00:00:00` 開始計時,中間如果系統時間被改變,則對應的時間也會被改變
* `CLOCK_MONOTONIC`: 從系統啓動時開始計算,利用jiffies來記錄,不會受到系統時間被改變的影響,但會因爲NTP的改變而受到影響
* `CLOCK_MONOTONIC_RAW`: 跟CLOCK_MONOTONIC一樣,但不會受到NTP的影響
* `CLOCK_PROCESS_CPUTIME_ID`: 記錄一個process裏所話的總CPU時間(User mode & Kernel mode),不包含等I/O和其他資源的時間。
* `CLOCK_THREAD_CPUTIME_ID`: 記錄一個thread所花的總CPU時間
有個時間結構體 `timespec`,其計時的單位是奈秒(nanosecond)
``` clike
strace timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
}
```
1. xtime: 從cmos電路中取得RTC的時間
2. jiffies: 從電腦開機到目前的總共時鍾中斷(time interrupt)次數
---
3. [time()](https://linux.die.net/man/2/time)
原型:
```clike
time_t time(time_t *t);
```
Description
time() returns the time as the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
4. [gettimeofday()](https://linux.die.net/man/2/gettimeofday)
原型:
```clike
int gettimeofday ( struct timeval * tv , struct timezone * tz )
```
跟time()一樣,但會回傳多一個timezone,看下面這個timeval的結構:
```clike
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
```
==Notes==:
The time returned by gettimeofday() is affected by discontinuous jumps in the system time (e.g., if the system administrator manually changes the system time). 如果系統時間被改變,那麼gettimeofday()所回傳的值,會被不連續的跳動時間所影響
---
1. 為什麼 time() 和 gettimeofday() 不適合拿來作 benchmark ?
在上面可以看到 ==time()== 跟 ==gettimeofday()== 都是直接回傳系統時間,所以如果系統時間在程式執行期間被改變,那麼回傳出來的值就會不準確。而且爲了要跟NTP同步,在同步的期間可能會有幾秒的誤差。
2. 爲什麼clock_gettime()結果飄忽不定?
==clock_gettime()== 會因爲不同的進程在CPU之間切換而受到影響。解決方法就是利用clock_gettime()找出95%信賴區間,這樣得到的數據就更加精準。
參考資料:
* [gettimeofday() should never be used to measure time](https://blog.habets.se/2010/09/gettimeofday-should-never-be-used-to-measure-time)
* [clock()、time()、clock_gettime()和gettimeofday()函数的用法和区别](http://www.cnblogs.com/krythur/archive/2013/02/25/2932647.html)
* [C library function: clock_gettime](http://oyh.logdown.com/posts/231106-c-clock-gettime)
* [What is the difference between CLOCK_MONOTONIC & CLOCK_MONOTONIC_RAW?](http://stackoverflow.com/questions/14270300/what-is-the-difference-between-clock-monotonic-clock-monotonic-raw)
* [Difference between CLOCK_REALTIME and CLOCK_MONOTONIC?](http://stackoverflow.com/questions/3523442/difference-between-clock-realtime-and-clock-monotonic)
* [容易混淆LINUX時鐘的xtime和jiffies](http://www.coctec.com/docs/linux/show-post-186188.html)
###### tags: `TempoJiJi` `ComputePi` `sysprog21`