# 2017q1 Homework1 (compute-pi) contributed by < `changyuanhua` > ### Reviewed by `baimao8437` * commit message 開頭應為大寫。 * 可以對一開始`make check`的各 method 時間結果做一些解釋。 * 可嘗試95信賴區間解決資料抖動問題。 * Leibniz 的程式碼中 `int x = (i%2) ? -1 : 1;`這行判斷拖慢了執行速度,可考慮其他方法來解決變號,e.g.`int x = -1; x*=-1;`。 * 最後一張 error rate 的圖片其實看不到很多 method 的結果,若有重疊或接近 0 的現象應特別解釋。 * 由於此作業對於計算 $\pi$ 會使用多種 method ,使得每新增一種,就得在 makefile 中多寫很多行,可嘗試簡化 makefile 已提升測試效率,可參考[makefile修改](https://github.com/0140454/compute-pi/commit/89ab12a144d0ce6d2d3c498e50277fe81c42b8d4)。 --- ## 開發環境 * 輸入指令 ` lscpu ` ``` Architecture: x86_64 CPU 作業模式: 32-bit, 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 每核心執行緒數:1 每通訊端核心數:4 Socket(s): 1 NUMA 節點: 1 供應商識別號: GenuineIntel CPU 家族: 6 型號: 94 Model name: Intel(R) Core(TM) i5-6300HQ CPU @ 2.30GHz 製程: 3 CPU MHz: 799.890 CPU max MHz: 3200.0000 CPU min MHz: 800.0000 BogoMIPS: 4608.00 虛擬: VT-x L1d 快取: 32K L1i 快取: 32K L2 快取: 256K L3 快取: 6144K NUMA node0 CPU(s): 0-3 ``` ## 作業要求 * 在 GitHub 上 fork [compute-pi](https://github.com/sysprog21/compute-pi),嘗試重現實驗,包含分析輸出 * 注意: 要增加圓周率計算過程中迭代的數量,否則時間太短就看不出差異 * 詳閱廖健富提供的[詳盡分析](https://hackpad.com/Hw1-Extcompute_pi-geXUeYjdv1I),研究 error rate 的計算,以及為何思考我們該留意後者 * 用 [phonebok](/s/S1RVdgza) 提到的 gnuplot 作為 `$ make gencsv` 以外的輸出機制,預期執行 `$ make plot` 後,可透過 gnuplot 產生前述多種實做的效能分析比較圖表 * 可善用 [rayatracing](/s/B1W5AWza) 提到的 OpenMP, software pipelining, 以及 loop unrolling 一類的技巧來加速程式運作 * 詳閱前方 "Wall-clock time vs. CPU time",思考如何精準計算時間 * 除了研究程式以外,請證明 [Leibniz formula for π](https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80),並且找出其他計算圓周率的方法 * 請詳閱[許元杰的共筆](https://embedded2015.hackpad.com/-Ext1-Week-1-5rm31U3BOmh) * 將你的觀察、分析,以及各式效能改善過程,並善用 gnuplot 製圖,紀錄於「[作業區](https://hackmd.io/s/Hy2rieDYe)」 ## baseline 版本 ```clike= double computePi_v1(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; } ``` * 因為積分區間為[0,1],N為這之中的分段,所以理論上N值越大,精準度就越高 ## 時間處理與 time 函式使用 * [相關資料](http://www.cnblogs.com/krythur/archive/2013/02/25/2932647.html) * [clock() 和 time() 的回傳值資料](http://pydoing.blogspot.tw/2010/07/c-stdtime.html) * [clock()](https://linux.die.net/man/3/clock) ``` clock_t clock(void); ``` * clock() 函数的返回值類型是 clock_t ,它除以 CLOCKS_PER_SEC 来得出时间 * clock()有3個問題: 1.超過一小時,會 overflow 2.沒有考慮 CPU 被子進程使用的情況 3.不能區分 user mode 和 kernel mode * 回傳程式開始執行後所使用的 cpu 時間 * [clock_gettime()](https://linux.die.net/man/3/clock_gettime) ``` int clock_gettime(clockid_t clk_id, struct timespec *tp); ``` * 在計時的時候,第一個參數 clk_id 可填入(ex. CLOCK_REALTIME ),而 struct timespec* tp 則是回傳的結果 * 有一個時間結構體 timespec,計時的單位是奈秒(nanosecond) ```clike= strace timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ } ``` * 是獲取 process 所經過的時間,不隨系统管理員修改了機器的時間而改變 * [time()](https://linux.die.net/man/2/time) ``` time_t time(time_t *t); ``` * 回傳日曆時間,也就是自 1970 年 1 月 1 日到現在所過的總秒數 * [gettimeofday()](https://linux.die.net/man/2/gettimeofday) ``` int gettimeofday ( struct timeval * tv , struct timezone * tz ) ``` * 有一個時間結構體 timeval,計時的單位精確到微秒 ```clike= struct timeval { time_t tv_sec; suseconds_t tv_usec; }; ``` * 如果系统管理員修改了機器的時間(調整日期),則所得的時間也會改變 * 四種函數比較 * clock() 的精確度是 10 毫秒 (ms) * time() 的精確度是 10 毫秒 (ms) * gettimofday() 的精確度是微秒 (μs) * clock_gettime() 的計量單位是奈秒 (ns) * 為什麼 clock_gettime() 結果飄忽不定? * 會因爲不同的 process 在CPU之間切換而受到影響 * 為什麼 time() 和 gettimeofday() 不適合拿來作 benchmark ? * 因為回傳的是系統時間,如果在執行期間改變的話,得到的值就不正確 ## [CPU bound](https://en.wikipedia.org/wiki/CPU-bound) 與 [I/O bound](https://en.wikipedia.org/wiki/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 速度。 * 很多時候我們可以使用 time 判斷程式是 CPU bound 還是 I/O bound。 1. `real < user`: 表示程式是 CPU bound,使用平行處理有得到好處,效能有提昇。 2. `real ≒ user`: 表示程式是 CPU bound,即使使用平行處理,效能也沒有明顯提昇,常見的原因有可能是其實計算量沒有很大,生成、處理、結束多執行緒的 overhead 吃掉所有平行處理的好處,或是程式相依性太高,不適合拿來作平行處理。 3. `real > user`: 表示程式是 I/O bound,成本大部分為 I/O操作,使得平行處理無法帶來顯著的效能提昇 ( 或沒有提昇)。 ## 操作 * 編譯指令 ```make check``` * 輸出 ``` gcc -c -O0 -std=gnu99 -Wall -fopenmp -mavx computepi.c -o computepi.o gcc -O0 -std=gnu99 -Wall -fopenmp -mavx computepi.o time_test.c -DBASELINE -o time_test_baseline gcc -O0 -std=gnu99 -Wall -fopenmp -mavx computepi.o time_test.c -DOPENMP_2 -o time_test_openmp_2 gcc -O0 -std=gnu99 -Wall -fopenmp -mavx computepi.o time_test.c -DOPENMP_4 -o time_test_openmp_4 gcc -O0 -std=gnu99 -Wall -fopenmp -mavx computepi.o time_test.c -DAVX -o time_test_avx gcc -O0 -std=gnu99 -Wall -fopenmp -mavx computepi.o time_test.c -DAVXUNROLL -o time_test_avxunroll gcc -O0 -std=gnu99 -Wall -fopenmp -mavx computepi.o benchmark_clock_gettime.c -o benchmark_clock_gettime ``` * Makefile 裡頭有` -fopenmp` `-mavx `,是作為指定開啟 OpenMP 和 AVX ``` CFLAGS = -O0 -std=gnu99 -Wall -fopenmp -mavx ``` * baseline ``` time ./time_test_baseline N = 400000000 , pi = 3.141593 1.25user 0.00system 0:01.25elapsed 99%CPU (0avgtext+0avgdata 1776maxresident)k 0inputs+0outputs (0major+82minor)pagefaults 0swaps ``` * openmp_2 ``` time ./time_test_openmp_2 N = 400000000 , pi = 3.141593 1.41user 0.00system 0:00.70elapsed 199%CPU (0avgtext+0avgdata 1868maxresident)k 0inputs+0outputs (0major+87minor)pagefaults 0swaps ``` * openmp_4 ``` time ./time_test_openmp_4 N = 400000000 , pi = 3.141593 1.55user 0.00system 0:00.41elapsed 373%CPU (0avgtext+0avgdata 1808maxresident)k 0inputs+0outputs (0major+89minor)pagefaults 0swaps ``` * avx ``` time ./time_test_avx N = 400000000 , pi = 3.141593 0.62user 0.00system 0:00.62elapsed 99%CPU (0avgtext+0avgdata 1704maxresident)k 0inputs+0outputs (0major+81minor)pagefaults 0swaps ``` * avxnuroll ``` time ./time_test_avxunroll N = 400000000 , pi = 3.141593 0.44user 0.00system 0:00.44elapsed 99%CPU (0avgtext+0avgdata 1856maxresident)k 0inputs+0outputs (0major+84minor)pagefaults 0swaps ``` * 編譯指令 ```make gencsv``` * 輸出 ```shell for i in `seq 100 5000 25000`; do \ printf "%d," $i;\ ./benchmark_clock_gettime $i; \ done > result_clock_gettime.csv ``` * 可視為 ```cstyles= for(i=100; i<25000; i+=5000) { printf("%d",i); benchmark_clock_gettime(i); } ``` * 編譯指令 ```time ./time_test_baseline``` * 輸出 ``` N = 400000000 , pi = 3.141593 real 0m1.276s user 0m1.272s sys 0m0.000s ``` * 顯示了三種時間:real,user和sys * real time : 也就是 Wall-clock time,當然若有其他程式同時在執行,必定會影響到。 * user time : 表示程式在 user mode 佔用所有的 CPU time 總和。 * sys time : 表示程式在 kernel mode 佔用所有的 CPU time 總和 ( 直接或間接的系統呼叫 ) * 我將 seq 100 5000 25000 改為 seq 500 500 200000 作圖後得 ![](https://i.imgur.com/iIVfnfL.png) * 從圖中發現 Baseline>openmp_2>avx>avx_unroll>openmp_4 ## 實驗一 : Leibniz formula for π * **proof** ![](https://wikimedia.org/api/rest_v1/media/math/render/svg/05ff8f37d93c9b5dafe66b5bdcf74a82d92d63fb) * Considering only the integral in the last line, we have: ![](https://wikimedia.org/api/rest_v1/media/math/render/svg/f3e1eaee64793a6fef77d480f00d3bf68e3270b5) * Therefore, by the squeeze theorem, as n → ∞ we are left with the Leibniz series: ![](https://wikimedia.org/api/rest_v1/media/math/render/svg/cfa16105f38678c4b8151cd1ac1cd1a0a8d219c6) * [Leibniz](https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80) * **Leibniz** ```clike= double compute_pi_leibniz(size_t N) { double pi = 0.0; for(int i=0; i<N; i++) { int x = (i%2) ? -1 : 1; pi += (double)x / (2*i +1) ; } return pi * 4.0; } ``` >請注意程式碼縮排 >[name=課程助教][color=red] >>ok, 謝謝[name=changyuanhua][color=black] * **Leibniz AVX SIMD** ```clike= double compute_pi_leibnizavx(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(2.0); ymm2 = _mm256_set1_pd(1.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); ymm3 = _mm256_mul_pd(ymm3,ymm1); ymm3 = _mm256_add_pd(ymm3,ymm2); ymm3 = _mm256_div_pd(ymm0,ymm3); ymm4 = _mm256_add_pd(ymm4,ymm3); } double tmp[4] __attribute__((aligned(32))); _mm256_store_pd(tmp, ymm4); pi += tmp[0] + tmp[1] + tmp[2] + tmp[3]; return pi * 4.0; } ``` * **Leibniz AVX SIMD + Unroll looping** ```clike= double compute_pi_leibnizavxunroll(size_t N) { double pi = 0.0; register __m256d ymm0, ymm1, ymm2, ymm3, ymm4, ymm5, ymm6, ymm7, ymm8, ymm9, ymm10; ymm0 = _mm256_set_pd(1.0,-1.0,1.0,-1.0); ymm1 = _mm256_set1_pd(2.0); ymm2 = _mm256_set1_pd(1.0); ymm7 = _mm256_setzero_pd(); ymm8 = _mm256_setzero_pd(); ymm9 = _mm256_setzero_pd(); ymm10 = _mm256_setzero_pd(); for (int i = 0; i <= N - 16; i += 16) { ymm3 = _mm256_set_pd(i , i+1.0 , i+2.0, i+3.0); ymm4 = _mm256_set_pd(i+4.0 , i+5.0 , i+6.0, i+7.0); ymm5 = _mm256_set_pd(i+8.0 , i+9.0 , i+10.0, i+11.0); ymm6 = _mm256_set_pd(i+12.0 , i+13.0 , i+14.0, i+15.0); ymm3 = _mm256_mul_pd(ymm1, ymm3); ymm4 = _mm256_mul_pd(ymm1, ymm4); ymm5 = _mm256_mul_pd(ymm1, ymm5); ymm6 = _mm256_mul_pd(ymm1, ymm6); ymm3 = _mm256_add_pd(ymm2, ymm3); ymm4 = _mm256_add_pd(ymm2, ymm4); ymm5 = _mm256_add_pd(ymm2, ymm5); ymm6 = _mm256_add_pd(ymm2, ymm6); ymm3 = _mm256_div_pd(ymm0, ymm3); ymm4 = _mm256_div_pd(ymm0, ymm4); ymm5 = _mm256_div_pd(ymm0, ymm5); ymm6 = _mm256_div_pd(ymm0, ymm6); ymm7 = _mm256_add_pd(ymm7, ymm3); ymm8 = _mm256_add_pd(ymm8, ymm4); ymm9 = _mm256_add_pd(ymm9, ymm5); ymm10 = _mm256_add_pd(ymm10, ymm6); } double tmp1[4] __attribute__((aligned(32))); double tmp2[4] __attribute__((aligned(32))); double tmp3[4] __attribute__((aligned(32))); double tmp4[4] __attribute__((aligned(32))); _mm256_store_pd(tmp1, ymm7); _mm256_store_pd(tmp2, ymm8); _mm256_store_pd(tmp3, ymm9); _mm256_store_pd(tmp4, ymm10); pi += tmp1[0] + tmp1[1] + tmp1[2] + tmp1[3] + tmp2[0] + tmp2[1] + tmp2[2] + tmp2[3] + tmp3[0] + tmp3[1] + tmp3[2] + tmp3[3] + tmp4[0] + tmp4[1] + tmp4[2] + tmp4[3]; return pi * 4.0; } ``` * **使用 gnuplot 作圖分析** ![](https://i.imgur.com/Hf0vb4k.png) * 從圖中發現 Baseline > leibniz > leibniz avx > openmp_2 > avx > leibniz avx unroll > avx_unroll > openmp_4 * leibniz avx 和 openmp_2 看起來相近 ## 實驗二 : Euler for π ![](https://wikimedia.org/api/rest_v1/media/math/render/svg/edf829fbf86ae73080bce0a95c497001b8d15fb1) * **code** ```clike= double compute_pi_euler(size_t N) { double pi = 0.0; for (size_t i = 1; i <= N; i++) pi += (1 / pow(i, 2.0)); pi *= 6; return sqrt(pi); } ``` * 之前程式碼因為 pow() 無法使用,所以就用最直接的方式表示之,但 sqrt() 不知道該怎呈現,所以找了資料,在 gcc 後加入 -lm 就可以解決就算加入 math.h 也不能使用 pow() 和 sqrt() 的問題 * **使用 gnuplot 作圖分析** ![](https://i.imgur.com/4Y6wSNC.png) * 從圖中發現 euler > Baseline > leibniz > leibniz avx > openmp_2 > avx > leibniz avx unroll > avx_unroll > openmp_4 ## 實驗三 : Nilakantha for π ![](https://wikimedia.org/api/rest_v1/media/math/render/svg/fdafa8bd24ce2b6fd518a3cf253ad1ef409388a6) * **code** ```clike= double compute_pi_nilakantha(size_t N) { double pi = 0.0; int x = 1; for(size_t i=1; i <= N; i++){ pi += x / ((2.0 * (double)i)*(2.0*(double)i+1.0)*(2.0*(double)i+2.0)); x *= (-1); } pi *= 4.0; pi += 3; return pi; } ``` * **使用 gnuplot 作圖分析** ![](https://i.imgur.com/5MOG01L.png) * 從圖中發現 euler > nilakantha > Baseline > leibniz > leibniz avx > openmp_2 > avx > leibniz avx unroll > avx_unroll > openmp_4 ## error rate * **code** ```clike= // Error rate calculation #define M_PI acos(-1.0) double pi = compute_pi(n); double diff = pi - M_PI > 0 ? pi - M_PI : M_PI - pi; double error = diff / M_PI; ``` * 觀看 [廖健富學長的筆記](https://hackpad.com/Hw1-Extcompute_pi-geXUeYjdv1I) ![](https://i.imgur.com/5CvVuVD.png) ![](https://i.imgur.com/IuaYR9C.png) ## 參考資料 * [TempoJiJi](https://hackmd.io/KwBgpgxghhAcsFpYgGyICwEYXqZgJgEYL4DMmYAnAEz4Bmm41QA=?view) * [shelly4132](https://hackmd.io/s/HJrLU0dp#) * [compute pi](https://en.wikipedia.org/wiki/Pi) * [廖健富學長](https://hackpad.com/Hw1-Extcompute_pi-geXUeYjdv1I) * [clock gettime()資料](https://starforcefield.wordpress.com/2012/08/06/c%E8%AA%9E%E8%A8%80%EF%BC%9A%E4%BD%BF%E7%94%A8clock_gettime%E8%A8%88%E7%AE%97%E7%A8%8B%E5%BC%8F%E7%A2%BC%E7%9A%84%E6%99%82%E9%96%93%E9%9C%80%E6%B1%82/)