# 2016q3 Homework#1 : phonebook ## Linux環境 對於Linux我在2年級去比叢集電腦的時候就有接觸了,電腦裏面也已經裝好Linux Mint了,剛好Mint也是Debian系列的,所以我就用Linux Mint來當作我的開發環境囉~ ![](https://hackpad-attachments.imgix.net/embedded2016.hackpad.com_5mwCincaZGR_p.571972_1456410611092_%E7%B3%BB%E7%B5%B1%E8%B3%87%E8%A8%8A_001.png?fit=max&w=882) - 這裡我遇到了一個小問題,我之前沒有更新版本所以我的Linux核心版本是unstable的發行版(3.8.11)所以找不到linux-tools,這讓我研究了好久...所以記得要把系統更新到最新喔 ```shell= $ sudo apt-get update //更新軟體列表 $ sudo apt-get dist-upgrade //更新Linux發行版本 ``` - 還有另一個問題我看了超不順眼...就是在linux上面執行chrome會有文字變成標楷體!我特地去找了方法來解決 其實只要把fonts-arphic-ukai 跟fonts-arphic-uming 解除安裝,我的思源黑體就回來了! ```shell= $ sudo apt-get remove fonts-arphic-ukai && sudo apt-get remove fonts-arphic-uming ``` ## Git & GitHub ### Git基本原理 首先,git的終極目標就是要能管理龐大的多人合作專案。那到底如何管理呢?全部都藏在`.git` 這個資料夾裡面了! git可以幫你儲存版本紀錄,每次提交新的版本,git都會把舊的版本存在.git裡面。git分成3個部份:工作目錄、索引、數據庫。在工作目錄中相檔案add進索引時會進行比對,找出更新的部份,再用commit提交進數據庫。 ### Git管理並上傳自己的repository 首先在網頁上fork老師的phonebook,然後用: ```shell= $ git clone your git url //譬如:git clone https://github.com/workfunction/phonebook.git ``` 先clone下來你自己的repository到你的電腦裏面,之後開始修改你要修改的檔案 接下來要對每個修改過的檔案用: ```shell= $ git add file you edit //譬如: git add phonebook_opt.c 或是用:git add . 提交全部的檔案進索引 ``` 這一步就是在把檔案註冊至索引中。 接著提交修改版本到git中(這裡都是在本地電腦裏面clone下來的repository做修改)用: ```shell= $ git commit -m "your change log title" //譬如: git commit -m "1st opt" ``` 這樣一來你就在電腦裡做好一個修訂版本囉! 注意一點如果打了上面的跳出: ```shell= 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啦,用下面指令 ```shell= $ git push origin master ``` 這樣就可以把你電腦裏面的修訂版本放到GitHub裏面了喔! 歐對要記得先做astyle再add喔!! ## Cache Miss分析 我看完cache miss的成因才真正了解問題的點在哪裡 下面是我對這個問題的分析 CPU內部有一個register跟3個層級的cache,下面是不同層級的cache到CPU所需的時間 從CPU到 大約需要的CPU周期 大約需要的時間 ```shell= 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的原因有下列三種: - Compulsory misses:也稱為cold start misses,第一次存取未曾在cache內的block而發生的cache miss。 - Conflict misses:發生在set-associative或direct-mapped caches,當多個blocks競爭相同的set。通常也稱作collision misses。 - Capacity misses:因為在程式執行期間,cache無法包含所有需要的block而產生的cache miss,也就是資料量過大無法同時存進cache。發生在一個block被取代後,稍後卻又需要用到,這個就是這次作業主要遇到的cache miss的原因。 CPU的一級數據緩存(L1 Data Cache)通常採用組相聯的方式來緩存數據,數據緩存是以Cache Line爲單位進行的,即緩存不命中時,相鄰的一組數據將被載入Cache Line,而不僅僅是當前數據。我的的處理器L1 Data Cache是8路組相聯的緩存,每個Cache Line大小爲64 Byte,總共是32 KByte。所以,爲減少Cache Miss的出現,每個指令讀取的數據最好是相鄰的。 ## 程式修改方面 ### 讓每個Last name在記憶體中的位置連續 > 因為main裡面有判斷OPT是否被定義過了 不然make plot會跑不出opt的數字喔[name=黃鏡清(Jason)] 我翻閱了幾個前面學長姐的實作,用刪減Struct的大小,只留下Last name跟指標讓cache裏面可以存更多Last name,於是我跟著這種想法實作。 > 理工科系的學生應該避免說「我也覺得很有道理」這種話,真相只有一個 ![](https://i.imgur.com/nIrT2of.png) 我把entry內容拆開,把另外的資訊刪掉並加了一個指標*pDetail指向令一個struct Detail但是執行結果... ![](https://i.imgur.com/O9uZ9Mn.png) 兩個時間都變長了阿!比對後原來螢光筆的那行其他人都沒有... ```clike= 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的順序,所以基本上效率不會提升, ![](https://i.imgur.com/FpaYMwc.png) 反而因為多了一個指標造成更多cache miss,所以真正要有效果的減少Capacity misses的排列方式應該是要像下面這樣: 這樣我一次cache就可以load 2個多一點的last name,cache miss的比例就會大幅縮減啦! ![](https://i.imgur.com/oEsMRqe.png) 所以應該還要在append的時候先把Entry的link list malloc出來,然後在獨立另一個函數malloc出後面的Detail才會有上面的記憶體排列,這樣新的append跟findName的時間都一起減少了 > 我目前還沒想到的方法沒辦法用append一個函數就完成連續排列,所以下面的圖是尚未malloc detail的append與findName的效果 ![](https://i.imgur.com/yIJ97N8.png) 所以有方向啦!現在可以讓資料都排在一起了有效的減少了Capacity misses的發生!接下來就是想辦法讓要快取的東西越小,減少Compulsory misses的發生囉! ### 縮小快取參照的大小 縮小快曲參照的方式最簡單的就是hash啦!因為CPU運算的時間比cache miss的時間還要短很多,所以用hash這個方法是用增加CPU運算的時間來減少cache的使用空間,可以說是時間複雜度增加而空間複雜度減少。只要一個hash table放在cache裏面,用CPU運算來算出last name有沒有在裏面就可以大幅減少cache miss了! 首先是設計hash table的結構,如下圖 ![](https://i.imgur.com/8HXIZ07.png) 我用一個array存了30000個指標指向linked list的第一個entry。然後我參考了王紹華同學的list建構方式,把新的entry插在list的最前面,這樣就不需要每次新增項目的時後還要跑到list的最後面在加入了! 再來是實作的部份,我新增了phonebook_hash.c & phonebook_hash.h,把append改成void回傳然後把傳入值改成傳入lastName跟hash table: ```clike= 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個結果可以同時輸出,最後跑出來的結果: >這裡也是參考王紹華同學的原始碼做修改的 ![](https://i.imgur.com/oi3944X.png) cache miss的比例從原本77.455%到縮減entry 45.499%到最後hash只剩10.025 %!append()的時間只有增加一點點,而findName()幾乎是瞬間完成了! ## C語言 & Makeflie的語法 > 我在修改整個專案讓3個結果可以同時顯示花了我很多心力,來跟大家分享一下吧 ### C語言的指示詞與Makefile在compile的配合 首先來說說我在這次作業學到的C語言特別用法:指示詞,指示詞其實我們一直在用就是在開頭要引入函式庫的#include跟要定義.h的時候的#ifndef都是指示詞的一種。這裡我用到很多#if defined () 跟 #else 還有 #elif 像是: ```clike= 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 的參數而已 ![](https://i.imgur.com/noc8y9Y.png) 所以3個compile出來的執行檔大小就完全不一樣啦! ### Makefile語法 Makefile在各種linux的程式上非常常見,最常見的用法是在編譯前給他電腦的環境參數,譬如系統、GCC、其他相依性軟體的位置等等。其實Makefile也有點像是bash,他都是在執行一段shell指令。所以Makefile真的是非常強大阿! 首先是變數宣告: ```ruby= CC ?= gcc CFLAGS_common ?= -Wall -std=gnu99 CFLAGS_orig = -O0 CFLAGS_opt = -O0 ``` Makefile的變數宣告像是js一樣不用給定變數型式,而`?=` 是如果沒有定義過再定義。 要叫用變數則要使用 `$(CC)` 或是 `${CC}` 這裡我有個小疑問,為甚麼CC跟CFLAGS_common要用`?=` 阿? 原來Makefile會自動去找環境變數,所以當環境變數裡面有定義過就不用在定義囉! 再來就是target,也就是makefile裡面類似function的區塊 ```ruby= 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 所指定的檔案,但不包含副檔名 - Makefile會先把整個檔案展開再決定變數值,所以變數會以最後定義的為主,如果想要讓他照Makefile裡面的順序,要把 `=` 改成 `:=` - define的用法跟定義變數最大的差別是define可以包含斷行(\n) ```ruby= define XXX ... ... ... endef ``` - 在指令前面加上 `@` 代表執行該指令但不顯示在tremal上面 - 前面加上 `-` 代表如果該指令出錯仍繼續執行Makefile - `.PHONY:target` 帶表這個target沒有輸出檔案,所以Makefile不會去檢查相依性 - Makefile會自動猜測編譯的檔案,如果要編譯foo.c要這樣寫: ```ruby= foo.o: common.h gcc -c foo.c 也可以直接寫成: foo.o: common.h ``` Makefile會自動依你的副檔名猜測如何編譯它! - Makefile裡面判斷式:`ifeq` `ifneq` `ifdef` `ifndef` 後面可以接 `else` 再用 `endif` 表示結束 - `include` 可以把後面指定的檔案全部引用進來 ## Gnuplot研究 > 參考陳博聖同學2016q1 Homework 1: gnuplot 學習 讓我受益良多 ```ruby= 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()) 第二行是來設定直方圖上面的顯示數字位置(x軸位置):(y軸位置) ## 參考資料 與 [王紹華](https://embedded2016.hackpad.com/ep/profile/HrlSjJ931Te) 和 [陳博聖](https://embedded2016.hackpad.com/ep/profile/nIcuQa8uYsP) 共同討論 [性能优化的方法和技巧:总结](http://www.kernelchina.org/content/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%9A%84%E6%96%B9%E6%B3%95%E5%92%8C%E6%8A%80%E5%B7%A7%EF%BC%9A%E6%80%BB%E7%BB%93) [从Java视角理解CPU缓存(CPU Cache)](http://astar.baidu.com/forum/forum.php?mod=viewthread&tid=460) [30 天精通 Git 版本](https://github.com/doggy8088/Learn-Git-in-30-days) [連猴子都能懂的Git入門指南](https://www.facebook.com/l.php?u=https%3A%2F%2Fbacklogtool.com%2Fgit-guide%2Ftw%2F&h=kAQHuJXzV&enc=AZOlF8WR2Hn5Af9dS28VCUg4Mdk6DOG66zUJm37bWYbvgVbi89mGF1yDj5h4ABtS4kdYKLmbKByx5aNjPlVYrR1DXl3fvDc90mbKwWhmu2UhWZhFRRLcU_pRnnAykadGExT8I8NfsuPkTRdqheJmbIM-tbn2yxJoCe298IIKZuThwjB0VatCMDHeM_f9yQC2t4c&s=1) [gnuplot.source](http://gnuplot.sourceforge.net/docs_4.2/node458.html) 資工wiki 老師youtube講解題目