對於Linux我在2年級去比叢集電腦的時候就有接觸了,電腦裏面也已經裝好Linux Mint了,剛好Mint也是Debian系列的,所以我就用Linux Mint來當作我的開發環境囉~
$ sudo apt-get update //更新軟體列表
$ sudo apt-get dist-upgrade //更新Linux發行版本
$ sudo apt-get remove fonts-arphic-ukai && sudo apt-get remove fonts-arphic-uming
首先,git的終極目標就是要能管理龐大的多人合作專案。那到底如何管理呢?全部都藏在.git
這個資料夾裡面了!
git可以幫你儲存版本紀錄,每次提交新的版本,git都會把舊的版本存在.git裡面。git分成3個部份:工作目錄、索引、數據庫。在工作目錄中相檔案add進索引時會進行比對,找出更新的部份,再用commit提交進數據庫。
首先在網頁上fork老師的phonebook,然後用:
$ git clone your git url //譬如:git clone https://github.com/workfunction/phonebook.git
先clone下來你自己的repository到你的電腦裏面,之後開始修改你要修改的檔案
接下來要對每個修改過的檔案用:
$ git add file you edit //譬如: git add phonebook_opt.c 或是用:git add . 提交全部的檔案進索引
這一步就是在把檔案註冊至索引中。
接著提交修改版本到git中(這裡都是在本地電腦裏面clone下來的repository做修改)用:
$ git commit -m "your change log title" //譬如: git commit -m "1st opt"
這樣一來你就在電腦裡做好一個修訂版本囉!
注意一點如果打了上面的跳出:
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
modified: main.c
modified: phonebook_opt.c
modified: phonebook_opt.h
no changes added to commit
代表他列的那些檔案被修改過可是你忘記add囉,所以就在把他列的檔案add進去吧!
最後就是上傳回去GitHub啦,用下面指令
$ git push origin master
這樣就可以把你電腦裏面的修訂版本放到GitHub裏面了喔!
歐對要記得先做astyle再add喔!!
我看完cache miss的成因才真正了解問題的點在哪裡 下面是我對這個問題的分析
CPU內部有一個register跟3個層級的cache,下面是不同層級的cache到CPU所需的時間
從CPU到 大約需要的CPU周期 大約需要的時間
Register 1 cycle
L1 Cache ~3-4 cycles ~0.5-1 ns
L2 Cache ~10-20 cycles ~3-7 ns
L3 Cache ~40-45 cycles ~15 ns
跨CPU傳輸 ~20 ns
Memory ~120-240 cycles ~60-120ns
Cache miss的原因有下列三種:
CPU的一級數據緩存(L1 Data Cache)通常採用組相聯的方式來緩存數據,數據緩存是以Cache Line爲單位進行的,即緩存不命中時,相鄰的一組數據將被載入Cache Line,而不僅僅是當前數據。我的的處理器L1 Data Cache是8路組相聯的緩存,每個Cache Line大小爲64 Byte,總共是32 KByte。所以,爲減少Cache Miss的出現,每個指令讀取的數據最好是相鄰的。
因為main裡面有判斷OPT是否被定義過了 不然make plot會跑不出opt的數字喔黃鏡清(Jason)
我翻閱了幾個前面學長姐的實作,用刪減Struct的大小,只留下Last name跟指標讓cache裏面可以存更多Last name,於是我跟著這種想法實作。
理工科系的學生應該避免說「我也覺得很有道理」這種話,真相只有一個
我把entry內容拆開,把另外的資訊刪掉並加了一個指標*pDetail指向令一個struct Detail但是執行結果…
兩個時間都變長了阿!比對後原來螢光筆的那行其他人都沒有…
entry *append(char lastName[], entry *e)
{
e -> pNext = (entry *) malloc(sizeof(entry));
e = e -> pNext;
strcpy(e -> lastName, lastName);
e -> pNext = NULL;
e -> pDetail = (detail *) malloc(sizeof(detail));
return e;
}
程式碼不要用「圖片」!直接用 hackpad 貼程式碼的功能,然後用「螢光筆」標注
改好了!
發現我在append裡面加了一段malloc出detail的大小根本沒有意義,在記憶體中的排列根本沒變:
假設我的cache大小剛好只能放一個原本的Entry下面情況有3個Entry,每次要找last name時把整個Entry load進cache裡面,當要找下一個的時候,下一個last name不在cache裡面造成cache miss(屬於Capacity misses),而藍色的部份沒有更動到last name的順序,所以基本上效率不會提升,
反而因為多了一個指標造成更多cache miss,所以真正要有效果的減少Capacity misses的排列方式應該是要像下面這樣:
這樣我一次cache就可以load 2個多一點的last name,cache miss的比例就會大幅縮減啦!
所以應該還要在append的時候先把Entry的link list malloc出來,然後在獨立另一個函數malloc出後面的Detail才會有上面的記憶體排列,這樣新的append跟findName的時間都一起減少了
我目前還沒想到的方法沒辦法用append一個函數就完成連續排列,所以下面的圖是尚未malloc detail的append與findName的效果
所以有方向啦!現在可以讓資料都排在一起了有效的減少了Capacity misses的發生!接下來就是想辦法讓要快取的東西越小,減少Compulsory misses的發生囉!
縮小快曲參照的方式最簡單的就是hash啦!因為CPU運算的時間比cache miss的時間還要短很多,所以用hash這個方法是用增加CPU運算的時間來減少cache的使用空間,可以說是時間複雜度增加而空間複雜度減少。只要一個hash table放在cache裏面,用CPU運算來算出last name有沒有在裏面就可以大幅減少cache miss了!
首先是設計hash table的結構,如下圖
我用一個array存了30000個指標指向linked list的第一個entry。然後我參考了王紹華同學的list建構方式,把新的entry插在list的最前面,這樣就不需要每次新增項目的時後還要跑到list的最後面在加入了!
再來是實作的部份,我新增了phonebook_hash.c & phonebook_hash.h,把append改成void回傳然後把傳入值改成傳入lastName跟hash table:
void append(char lastName[], entry *table[])
{
unsigned nHash = hash(lastName);
entry *e = (entry *) malloc(sizeof(entry));
e -> pNext = table[nHash];
if(!table[nHash]) {
table[nHash] = e;
} else {
table[nHash] -> pNext = e;
}
strcpy(e -> lastName, lastName);
}
標注螢光筆的地方很重要!我一開始沒有想到第一個放進去的entry會有問題所以compile完執行之後出現segmentation fault。第一次遇到這種情況,我又不會用gdb(今天看到老師要教耶開心~),所以我只能每行每行加上printf慢慢找問題在哪裡,結果就如上面說的啦,在還沒有東西的時候用table[nHash] -> pNext = e會讓程式改到其他地方的記憶體位置,所以就segmentation fault了。
然後我修改了main.c calculate.c runtime.gp讓3個結果可以同時輸出,最後跑出來的結果:
這裡也是參考王紹華同學的原始碼做修改的
cache miss的比例從原本77.455%到縮減entry 45.499%到最後hash只剩10.025 %!append()的時間只有增加一點點,而findName()幾乎是瞬間完成了!
我在修改整個專案讓3個結果可以同時顯示花了我很多心力,來跟大家分享一下吧
首先來說說我在這次作業學到的C語言特別用法:指示詞,指示詞其實我們一直在用就是在開頭要引入函式庫的#include跟要定義.h的時候的#ifndef都是指示詞的一種。這裡我用到很多#if defined () 跟 #else 還有 #elif 像是:
FILE *output;
#if defined(OPT)
output = fopen("opt.txt", "a");
#elif defined(HASH)
output = fopen("hash.txt", "a");
#else
output = fopen("orig.txt", "a");
#endif
這些功能跟一般if else的判斷式功能上沒有甚麼不同 差別在於指示詞是給compiler看的,if else是在執行的時候才判斷的,所以當指示詞條件成立時,其他區塊的code不會被compile,在執行的時候就不會多浪費時間去判斷了!因為我的hash方法函式傳入值不同,所以在append跟findname我都有用指示詞判斷多寫出一塊專門給hash用的code,然後在Makefile裡面用-D來定義一個參數再用指示辭去判斷這個參數有沒有被定義。
理工科系的學生不要沒事就說「的感覺」這種話!
說話要精確,這不是「類似 function overloading」。只是 preprocessor 的參數而已
所以3個compile出來的執行檔大小就完全不一樣啦!
Makefile在各種linux的程式上非常常見,最常見的用法是在編譯前給他電腦的環境參數,譬如系統、GCC、其他相依性軟體的位置等等。其實Makefile也有點像是bash,他都是在執行一段shell指令。所以Makefile真的是非常強大阿!
首先是變數宣告:
CC ?= gcc
CFLAGS_common ?= -Wall -std=gnu99
CFLAGS_orig = -O0
CFLAGS_opt = -O0
Makefile的變數宣告像是js一樣不用給定變數型式,而?=
是如果沒有定義過再定義。
要叫用變數則要使用 $(CC)
或是 ${CC}
這裡我有個小疑問,為甚麼CC跟CFLAGS_common要用?=
阿?
原來Makefile會自動去找環境變數,所以當環境變數裡面有定義過就不用在定義囉!
再來就是target,也就是makefile裡面類似function的區塊
all: $(EXEC)
SRCS_common = main.c
phonebook_orig: $(SRCS_common) phonebook_orig.c phonebook_orig.h
$(CC) $(CFLAGS_common) $(CFLAGS_orig) \
-DIMPL="\"$@.h\"" -o $@ \
$(SRCS_common) $@.c
phonebook_opt: $(SRCS_common) phonebook_opt.c phonebook_opt.h
$(CC) $(CFLAGS_common) $(CFLAGS_opt) \
-DIMPL="\"$@.h\"" -o $@ \
-DOPT=0 \
$(SRCS_common) $@.c
phonebook_hash: $(SRCS_common) phonebook_hash.c phonebook_hash.h
$(CC) $(CFLAGS_common) $(CFLAGS_opt) \
-DIMPL="\"$@.h\"" -o $@ \
-DHASH=0 \
$(SRCS_common) $@.c
target就是螢光筆劃起來的區塊開頭,all的意思就是當你執行make
的時候要執行哪些target,其他如果要執行一個特定的區塊就打make phonebook_orig
就可以執行phonebook_orig這個target了。
再來是:
後面的東西是指相依性的檔案,Makefile會去比較輸出的檔案跟後面給定相依性檔案的時間關係,如果在:後面放的是原始碼的檔案,比較後發現輸出檔案比較新,表示原始碼沒有被改過,就不用在重新編譯一次,所以這一段target就會被跳過不執行。
後面的指令要用<tab>開頭代表指令。
$@
是一個代名詞,代表target的名字。$?
是代表比target還新的相依性檔案中已更新的值$<
代表第一個相依性檔案$*
targets 所指定的檔案,但不包含副檔名=
改成 :=
define XXX
...
...
...
endef
@
代表執行該指令但不顯示在tremal上面-
代表如果該指令出錯仍繼續執行Makefile.PHONY:target
帶表這個target沒有輸出檔案,所以Makefile不會去檢查相依性
foo.o: common.h
gcc -c foo.c
也可以直接寫成:
foo.o: common.h
Makefile會自動依你的副檔名猜測如何編譯它!
ifeq
ifneq
ifdef
ifndef
後面可以接 else
再用 endif
表示結束include
可以把後面指定的檔案全部引用進來參考陳博聖同學2016q1 Homework 1: gnuplot 學習 讓我受益良多
reset
set ylabel 'time(sec)' `設定Y軸名稱`
set style fill solid `設定圖形屬性 fill solid是直方圖`
set title 'perfomance comparison' `設定設定圖片標題`
set term png enhanced font 'Verdana,10' `設定圖片的字體`
set output 'runtime.png' `設定輸出檔名`
plot [:][:0.060]'output.txt' using 2:xtic(1) with histogram title 'original', \
'' using ($0-0.1):($2+0.002):2 with labels title ' ', \
'' using 3:xtic(1) with histogram title 'optimized' , \
'' using ($0+0.1):($3+0.002):3 with labels title ' ', \
'' using 4:xtic(1) with histogram title 'hashed' , \
'' using ($0+0.3):($4+0.0015):4 with labels title ' '
using 2:xtic(1) with histogram title 'original',
'' using ($0-0.1):($2+0.002):2 with labels title ' ',
using 2
的意思是以output.txt的第二欄當作資料, xtic(1)
是用第一欄當作X軸的標題(就是append()跟findname())與 王紹華 和 陳博聖 共同討論
性能优化的方法和技巧:总结
从Java视角理解CPU缓存(CPU Cache)
30 天精通 Git 版本
連猴子都能懂的Git入門指南
gnuplot.source
資工wiki
老師youtube講解題目