contributed by <petermouse
>
溫習去年秋季做的共筆並繼續改進
hunng
每次看 Makefile 都很新奇,以前只用過最基本的,但 Makefile 遠不只這些!
紀錄一下不會的 (網路參考資料)
ifeq
ifneq
ifdef
ifndef
等 if 述句,也可以有對應的 else
,結尾以endif
表示:=
: =
內容裡的變數更動後會隨之更動,:=
避免這種狀況+=
: 很直覺地代表參數的串接?=
: 沒有 define 才指定%.c
: 所有.c
結尾的名稱,很像終端機中的 *
$<
: 第一個 dependencies 之值 ( i.e. 冒號後的第一個)$(strip)
代表將字串省去開頭結尾的空白make 時不加 PROFILE=1
時執行 raytracing
# Rendering scene
Done!
Execution time of raytracing() : 2.495271 sec
再 PROFILE=1
產生 gprof 的相關資料 (gmon.out)
# Rendering scene
Done!
Execution time of raytracing() : 5.125118 sec
時間要原本的兩倍左右
使用 gprof
分析函數間的執行時間以及執行次數
gprof
使用時同時包含 image-file 以及 profile-file
$ gprof -b raytracing gmon.out | less
產生的部份結果如下:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
22.18 0.47 0.47 69646433 0.00 0.00 dot_product
17.94 0.85 0.38 56956357 0.00 0.00 subtract_vector
11.80 1.10 0.25 10598450 0.00 0.00 normalize
9.68 1.31 0.21 13861875 0.00 0.00 rayRectangularIntersection
7.08 1.46 0.15 31410180 0.00 0.00 multiply_vector
6.37 1.59 0.14 17836094 0.00 0.00 add_vector
多數的時間都發生在數學上的運算,比如前三名 dot_product()
subtract_vector()
normalize
就佔了 50% ,而且被呼叫的次數極多,從這裡下手對效能也很大的影響
再來觀察 perf
對原本程式(未使用 PROFILE=1
)的結果
Performance counter stats for './raytracing':
121,000 cache-references
41,214 cache-misses # 34.061 % of all cache refs
1,870,361,272 branches
5,298,723 branch-misses # 0.28% of all branches
2.475591343 seconds time elapsed
branch 數量竟然高達了18億!branch 除了增加判斷的成本,還有預測錯誤遭造成的懲罰,如果能夠降低 branch 的數量也預期會有更好的表現。
在 math-toolkit.h
中,有多個 function 內都有固定次數的 for 迴圈
static inline
void subtract_vector(const double *a, const double *b, double *out)
{
for (int i = 0; i < 3; i++)
out[i] = a[i] - b[i];
}
不是很必要去宣告一個變數 i,判斷 i 是否大於 3,或是對 i 加 1,類似的都可以改成
static inline
void subtract_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];
}
以及最重的 dot_product
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;
}
可以直接寫成 return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
時間比起最初要快了不少
# Rendering scene
Done!
Execution time of raytracing() : 1.687605 sec
gprof 的結果,有做 loop unrolling 的 function 都把時間壓低了
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
20.78 0.25 0.25 56956357 0.00 0.00 subtract_vector
13.14 0.40 0.16 69646433 0.00 0.00 dot_product
11.87 0.54 0.14 10598450 0.00 0.00 normalize
9.33 0.65 0.11 13861875 0.00 0.00 raySphereIntersection
7.63 0.74 0.09 4620625 0.00 0.00 ray_hit_object
6.78 0.82 0.08 31410180 0.00 0.00 multiply_vector
6.78 0.90 0.08 13861875 0.00 0.00 rayRectangularIntersection
5.09 0.96 0.06 17836094 0.00 0.00 add_vector
4.66 1.02 0.06 17821809 0.00 0.00 cross_product
4.24 1.07 0.05 1048576 0.00 0.00 ray_color
1.70 1.09 0.02 3838091 0.00 0.00 length
1.70 1.11 0.02 1204003 0.00 0.00 idx_stack_push
1.70 1.13 0.02 1048576 0.00 0.00 rayConstruction
1.27 1.14 0.02 4221152 0.00 0.00 multiply_vectors
perf 的結果:
Performance counter stats for './raytracing':
92,736 cache-references
36,946 cache-misses # 39.840 % of all cache refs
969,826,972 branches
3,103,684 branch-misses # 0.32% of all branches
1.726622426 seconds time elapsed
branch 數直接少一半!
簡單地估算一下,i 每次 function call 需判斷 4 次 ( i = 0 ~ i = 3)
( 56956357 + 69646433 + 31410180 + 17836094 + 4221152 ) * 4 = 720280864
至少少了7億次,實際上少更多
看看那些 function 加起來可是呼叫了幾億次!,如果全部 inline 就可以減少呼叫的成本,但缺點是程式碼會很長,增加整體的大小
因為要強制在 -O0
的情形下 inline ,在 declarator 前要加 gcc 的 attribute 來強制執行
static inline __attribute__((always_inline))
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];
}
gprof 中已經看不見這些 function
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
31.55 0.41 0.41 13861875 0.00 0.00 rayRectangularIntersection
29.24 0.79 0.38 13861875 0.00 0.00 raySphereIntersection
16.16 1.00 0.21 2110576 0.00 0.00 compute_specular_diffuse
6.93 1.09 0.09 1048576 0.00 0.00 ray_color
3.08 1.13 0.04 2110576 0.00 0.00 localColor
執行時間少了約 0.2 秒
# Rendering scene
Done!
Execution time of raytracing() : 1.456480 sec
OpenMP 可以簡單地開啟多個 thread 完成工作,只要在適當的地點加入pragma omp
,並在編譯連結時加入 -fopenmp
即可
程式主體為 raytracing()
,再看看 raytracing()
內其實由雙層 for 迴圈,對圖像的每一個點(x,y) 依序建立,像素之間沒有什麼相依關係,只要小心有些變數不能共用就好
以 row 為最小單位,對於最外層的迴圈平行化
#pragma omp parallel for private(stk, object_color)
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
/* statements */
}
}
不過建出來是錯的,呈現就像下圖一樣,還有共用的變數少考慮了!
少讓 d
變數變成私有
#pragma omp parallel for private(d, stk, object_color)
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
/* statements */
}
}
在沒有指定 thread 數量情況下的結果
# Rendering scene
Done!
Execution time of raytracing() : 0.763289 sec
執行時間降為一半!
考慮不同 thread 數量的執行時間,繪製成下圖
在 2 與 4 個 thread 數量時間是差不多的, 8 以後雖然差不多,但是時間有在稍微將低一點。
參考2016春季班 team4 共筆中的圖片複雜度分析結果,可以發現每個點做的運算量都不太一樣,部份點比起其他點更耗時,看起來是個熱點!如果在平行時能夠 load balancing,就減少 thread 等待其他 thread 的時間,加快整體的速度
OpenMP loop scheduling 共分為 5 種
OMP_SCHEDULE
決定 static dynamic guided未說明 loop scheduling 由系統的 def-sched-var
決定, def-sched-var
的值對應的方法也是 implementation-defined
嘗試使用 static,並將 chunk size 設定為 1,也就是 cyclic partition
程式碼如下
#pragma omp parallel for if(OMP_THREADS - 1) num_threads(OMP_THREADS) \
private(d, stk, object_color) schedule(static, 1)
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
/* statements */
}
}
與原本的相比
在 4 個 thread 數量的效能增加很多!,原本的圖片在切割成 4 等分是最不平衡的,現在稍微平衡了一點。
其實這裡忘了先確認 default 的分配方式,以上只是猜測 default 直接將 for 迴圈切成等分分配給每個 thread ,才會在 4個 thread 時無法獲得很好的效能
先寫一個小程式看看結果
#include <stdio.h>
#include <omp.h>
int main()
{
int i = 0;
#pragma omp parallel for num_threads(4)
for (i = 0; i < 8; i++) {
printf("%d:%d\n", omp_get_thread_num(), i);
}
return 0;
}
結果輸出
thread 0 : 0
thread 0 : 1
thread 1 : 2
thread 1 : 3
thread 3 : 6
thread 3 : 7
thread 2 : 4
thread 2 : 5
看來 default 就如猜想一樣,是直接切割等分分配 chunk
再來驗證 default 4 threads 情形下,是不是因為 thread 2 (熱點最高)拖累整體速度
#pragma omp parallel num_threads(OMP_THREADS)
{
double time1,time2;
time1 = omp_get_wtime();
#pragma omp for private(d, stk, object_color)
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
/* statements */
}
time2 = omp_get_wtime();
printf("thread %d: %lf\n",omp_get_thread_num(), time2-time1);
}
}
每一列做完便會輸出 thread id 及累計時間,執行後的最後幾行輸出
thread 2: 0.751149
thread 2: 0.754657
thread 1: 0.755651
thread 2: 0.758141
thread 1: 0.760791
thread 2: 0.762016
thread 2: 0.765384
thread 2: 0.768642
thread 2: 0.771855
thread 2: 0.775030
其實 thread 0 與 thread 3 (熱點較少)已經先完成了,thread 1 與 thread 2 還在做
這樣測量有點不準確,因為 CPU 先挑選哪個 thread 執行也無法確定,不過多次測試情況下都由 thread 2 最後執行完,(static, 1)沒有這個問題 petermouse
再來試試 dynamic,load balancing 交由程式以一列為單位自動分配
#pragma omp parallel for if(OMP_THREADS - 1) num_threads(OMP_THREADS) \
private(d, stk, object_color) schedule(dynamic, 1)
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
4 與 8 個 thread 小幅度地改善,其他的與 static 相近
gnuplot 柱狀圖製作
2016春季班 team4 共筆
omp for scheduling
openmp loop scheduling