# 2016q1 Homework1 (phonebook) ## 環境架設 因為在老師推薦lubuntu之前就灌了雙系統,選用Ubuntu 14.0.4,之前有灌過作業系統,但是是直接全部刷掉,所以每一步都很隨便,這次要灌雙系統,如果踏錯一步可能就回天乏術(怕怕),特別是在分割磁碟的地方特別小心: 分割了50GB給Ubuntu的根目錄 / 以及 2GB 給 SWAP ![](https://i.imgur.com/HsPZII7.png) ## Ubuntu grub 在安裝雙系統時,設定開機選項為Ubuntu的grub,因為看網上好像有人因為選Windows boot manager而找不到linux的開機口,雖然選擇了grub,而grub預設開機選項為Ubuntu,而且只有10秒的時間可以選擇,但是我平常還是會使用windows,因此會有需求開機預設為windows,查了一下: * 主要選單檔,/boot/grub/grub.cfg,不應再被手動編輯,即使是由「root」身份 * grub.cfg 會在任何有更新、核心被加入/移除或是使用者執行 update-grub 的時候被覆寫 * 主要用來改變顯示設定的設定檔是 /etc/default/grub ```shell GRUB_DEFAULT=0 # 設定預設選單選項 在 /boot/grub/grub.cfg 中的第一筆「選單選項」為 0,第二筆為 1,以此類推 GRUB_TIMEOUT=10 # 多久之後自動以預設作業系統開機 ``` 我主要是改這兩個設定而已,再執行# sudo update-grub ,更動將會自動寫入grub.cfg 結果圖: ![](https://i.imgur.com/ppKgaB6.jpg) 參考資料: [GRUB2中文指南第二版](http://wiki.ubuntu-tw.org/index.php?title=GRUB2%E4%B8%AD%E6%96%87%E6%8C%87%E5%8D%97%E7%AC%AC%E4%BA%8C%E7%89%88%28%E4%B8%8A%EF%BC%89#.E8.A8.AD.E5.AE.9A_GRUB_2) > GRUB 2 是在 10.04 以後才於許多作業系統中使用 ## 修改Terminal顏色: 參考了[DADA的筆記](https://embedded2016.hackpad.com/2016q1-Homework-1-YkqjhwgnQcA) ,找到這篇[BASH color prompt](https://richardnotes.wordpress.com/2011/05/15/bash-color-prompt/) 才知道可以改Terminal的顏色 ## 修改vim 參考自 [凍仁的筆記](http://note.drx.tw/2008/01/vimrc-config.html) vim是vi的進化,再編輯檔案時,會用顏色來編式不同的文˙字,可是跟以前用的編輯器(ex.notepad++)之類的比起來,不能自動縮排讓手酸的要命,所以就來搬救兵了 照著這篇,我加了這個設定 ```shell set ai " 自動縮排 set tabstop=4 " 自訂縮排 (Tab) 位元數 set shiftwidth=4 " 自訂縮排 (Tab) 位元數 符合格式 ``` > 本來有加入set number顯示行號,結果複製程式碼時,行號也過來了,所以乾脆關掉 # 案例分析 Makefile學習 > 之前摸Linux時是碰一套叫LAMMPS的科學計算軟體,makefile只是把路徑填進去,未深入了解 老師提供的[教學](http://tetralet.luna.com.tw/?op=ViewArticle&articleId=185),[其他教學](http://kevincrazy.pixnet.net/blog/post/29780477-makefile%E7%B0%A1%E6%98%93%E6%95%99%E5%AD%B8...) 以下是Makefile的基本編譯格式: ```shell target: dependencies1 dependencies2.... <tab>commands... target: 要建立的檔案 dependency: 相依檔案 若相依檔案時間都比tartget舊,則不進行編譯 commands: 一定要以一個縮排開頭 ``` * make 指令會參考 all 這個目標項目,並依據它的dependencies 來決定要建立哪些項目,EX.作業中的all會導向orig跟opt的編譯,如果沒有all則編譯第一項target * 如果dependency 不存在於資料夾中,就無從比對新舊,所以就會在Makefile中尋找此target,找到會先執行 * 如果有多個Makefile 可用 # make -f MakefileName * -DIMPL="\"$@.h\"" * 有define的作用,定義IMPL=某個字串 * $@:代表 targets * 另外,/$^代表所有dependency(不同於$<:第一個dependency) ### assert function(main.c) * 在研究main.c時,發現assert這個沒見過的function assert(findName(input0, e) && "Did you implement findName() in " IMPL "?"); assert(0 == strcmp(findName(input0, e)->lastName, "zyxel")); ~~查到資料說:"當自己寫的函式庫要提供他人使用時,適當的利用維護敘述,可以建立安全的使用介面,避免他人因為使用不當,而造成不可預期的後果",所以若assert後面的判斷句為否,則程式停止ˋ,可以避免後續錯誤~~ > 請讀第一手的資料: http://linux.die.net/man/3/assert > 這裡寫說:assert是幫助程式設計師debug用的,所以其實這個部份對於user來說是無用的,所以如果是完整版的程式,就可以把assert function砍掉,省時間囉~ > 不需要移除,只要變更編譯參數即可,請見 [man assert](http://linux.die.net/man/3/assert) > 收到!!在Makefile中新增: CFLAGS_common ?= -Wall -std=gnu99 -DNDEBUG > 而這裡應該是為了避免findname()的錯誤,例如沒有更改phonebook—opt.c中的的return NULL的話,就會產生中斷,但是缺點是會執行多餘的程式,看起來會多執行兩次findname,以下是orig版的cache-miss > 文字訊息不要用「圖片」! ![](https://i.imgur.com/xaZlVM0.png) 我認為既然都是執行findname,應該也會佔用多餘的執行˙間,以及造成更多cache-miss,~~所以將main.c的assert都註解掉~~,在編譯參數中加入"-DNDEBUG",這樣編譯時assert就不會被編譯出來,下是其cache-miss > 不要把 assert 移除,變更編譯參數即可! ![](https://i.imgur.com/cEpedom.png) 由此可見,assert的部份的確會造成更多的cache-miss,我認為,在findname()是被完整的其況下並不太需要做assert的動作,省去這個部份的程式碼,也不算影響其原功能 或者是在做保護測試的同時,也能避免過多的cache-miss->簡短findname() # 優化方式 ## 使用體積較小的 struct ### orig版本的entry ```c typedef struct __PHONE_BOOK_ENTRY { char lastName[MAX_LAST_NAME_SIZE]; char firstName[16]; char email[16]; char phone[10]; char cell[10]; char addr1[16]; char addr2[16]; char city[16]; char state[2]; char zip[5]; struct __PHONE_BOOK_ENTRY *pNext; } entry; ``` 計算struct: 16*5+10*2+5+2+(alignment)+8(pointer) 字串陣列總合是123 由於此struct最大元素為8byte,所以alignment是8byte,所以123要加上5btye,最後加上8byte指標=136 * 程式驗證 ``` sizeof("%d\n",(int)entry) ``` entry大小為136byte,很快就會佔滿cache,cahce-miss也就很多,如果能減少entry大小,cache就能容納更多資料,cache-miss也能減少 因為在執行findName()時只會用到lastName,所以把其他資料包進detail裏面 ```c typedef struct __PHONE_BOOK_ENTRY_DETAIL { char firstName[16]; char email[16]; char phone[10]; char cell[10]; char addr1[16]; char addr2[16]; char city[16]; char state[2]; char zip[5]; } detail; /* original version */ typedef struct __PHONE_BOOK_ENTRY { char lastName[MAX_LAST_NAME_SIZE]; struct __PHONE_BOOK_ENTRY_DETAIL *pDetail; struct __PHONE_BOOK_ENTRY *pNext; } entry; ``` 新的entryˇ大小為32byte Performance counter stats for ’./phonebook_opt1’ (100 runs): ``` 353,109 cache-misses # 80.672 % of all cache refs 453,700 cache-references 206,288,675 instructions # 1.70 insns per cycle 125,162,971 cycles 0.055469342 seconds time elapsed ( +- 1.52% ) ``` cache-miss由93% -> 80%,執行時間也有縮短,這些節省的時間就是原本受cache-miss影響。 ![](https://i.imgur.com/xeNBCM4.png) 使用 Hash funtion * 概念: 給定一個Char array 透過hash function的運算,可以放入hash table的array,如果hash value重複 就在後面建linked list,搜尋時同樣將key輸入hash function ![](https://i.imgur.com/1gfTFyQ.png) 我第一次使用的hashfunction和hashtable: SIZE = 100 ``` int mod = 100; unsigned sum; int i; for (sum=1, i=0; i < 16&&key[i]>0; i++){ if(key[i]>0) sum += key[i]; //加總ASCIIcode當hash value } return sum % mod; ``` hashtable append() ```c entry *append(char lastName[], entry *hashtable[]) { entry *e; e = (entry *) malloc(sizeof(entry)); strcpy(e->lastName, lastName); e->pNext = NULL; unsigned id = hash_fun(lastName); if (!hashtable[id]) { hashtable[id]=e; } else { entry *temp = hashtable[id]; while(temp->pNext) { temp= temp->pNext; } temp->pNext = e; } return e; } ``` 結果跑了兩分鐘,append()都沒結束(冏),果然35萬筆資料不是蓋的QQ,每個,這次改成array大小為300,同時也要更改hashfunction的mod值, ``` int mod = 300; unsigned sum; int i; for (sum=1, i=0; i < 16&&key[i]>0; i++){ if(key[i]>0) sum += key[i]; } return sum % mod; ``` 發現時間還是太久了,但是原本這個hashfunction產生的值大約在5百多,所以要再加快的話,勢必要用不一樣function, 第三次改成將ASCIIcode做相乘,hash value平均大約到8千萬,以3萬的array去做hash table ```c int mod = 300000; unsigned sum; int i; for (sum=1, i=0; i < 16&&key[i]>0; i++){ if(key[i]>0) sum *= key[i]; //ASCIIcode 相乘 } return sum % mod; ``` 跑出來成績沒有別人好,但是已經是大進步了 跟自己比! execution time of append() : 0.198321 sec execution time of findName() : 0.000032 sec make cache-miss 1,589,357 cache-misses # 65.627 % of all cache refs 2,603,703 cache-references 284,554,622 instructions # 0.61 insns per cycle 467,728,346 cycles 我把hash table每格的第一個entry印出來,結果卻是:有hash到跟null比起來大約只有1:3,這樣子就會有很多空的entry,而且array中的linked_list會太長,要再找分布更均勻的hashfunction 查到這個djb2 algorithm,這是一個很有名的String hash function DJB2 優點: 選用shift當成乘法,加快運算 hash值相當均勻分布 至於為什麼選5381和33這兩個數字,好像沒有人能有個合適的解釋,只能說33是由測試中找出來最適合的數字 djb2 hash function ```c unsigned hash_fun(char key[]) //djb2 hash function { int mod = TABLE_SIZE; unsigned hash = 5381; int i=0; while (key[i]>0) { hash = ((hash << 5) + hash) + key[i++]; /* hash * 33 + ASCII */ } return hash % mod ; } ``` 可以看到幾乎所有array都有hash到,所以linked list搜尋長度自然也減小,減少excutiontime和cache-miss 另外,因為append時間過長,主因是在延伸linked list時,需要向後爬很久,和 王紹華 討論後,發現可以把新增的資料放在第1個entry這樣就不必每次都要爬到最後了 新的append(): ``` entry *append(char lastName[], entry *hashtable[]) { entry *e = (entry *) malloc(sizeof(entry)); strcpy(e->lastName, lastName); e->pNext = NULL; unsigned id = hash_fun(lastName); if(!hashtable[id]) { hashtable[id]=e; } else { e->pNext = hashtable[id]->pNext; hashtable[id]->pNext = e; } return e; } ``` target: zyxel find zyxel execution time of append() : 0.086690 sec execution time of findName() : 0.000026 sec size of entry : 32 bytes 646,316 cache-misses # 51.121 % of all cache refs 1,317,499 cache-references 263,443,203 instructions # 1.32 insns per cycle 207,572,891 cycles -orig opt1 opt2執行時間比較 一如預期,因為透過hash_function快速的找到目標linked list,也減小list長度,所以findName()的執行時間超極少,但是append也因此增長了,由這個圖來看的話,hash的優化方法甚至式最差的,因為把總執行時間加總起來,是OPT2最久QQ,但是回頭想一想,hashtable的建製雖然耗時,但是只要建立完成後,不管查幾筆資料都是超快速,所以以長遠來看,當然還是hash_opt顯勝 orig OPT1 OPT2 cache-miss: 91% 80% 51% append() 0.057 0.048 0.069 findName() 0.0057 0.003255 0.00003 為此我會多做多幾比查詢,另外,我一直覺得,如果每次都是找預設的"zvbel",這樣不準的,應該要亂數隨機挑選查詢,才比較公平 更改main.c,在append()後,會進行10次random的查詢: 可見findName部份,opt2幾乎沒增加, ## gnuplot 學習 gnuplot是一個免費且功能強大的繪圖軟體,但都是由文字指令來使用,也因為這樣不同的組合會有無限可能,製造出自己想要的圖表 這是我最後的runtime.gp 本來也是看不懂,所以試著改,沒想到改對了,但還是不知道在幹嘛。後來看過別人的hachpad ,才比較懂一些: ``` reset # 清除之前所有的設定 set ylabel ’time(sec)’ # y軸座標 set style fill solid # 圖形填滿 Set style fill set title ’perfomance comparison’ # 圖表標題 set term png enhanced font ’Verdana,10’ # 輸出png設定 term: terminal set term png # 圖表內字型 set output ’runtime.png’ # 輸出檔案 plot [:][:0.10]’output.txt’\ # 引入檔案 設置XY軸數據範圍 using 2:xtic(1) with histogram title ’original’, \ # * Orig資料 ’’ using 3:xtic(1) with histogram title ’optimized1’ ,\ # * opt1資料 ’’ using 4:xtic(1) with histogram title ’optimized2’ ,\ # * opt2資料 ’’ using ($0-0.06):($2+0.0015):2 with labels title ’’, \ #** orig顯示數值 ’’ using ($0+0.2):($3+0.0015):3 with labels title ’’, \ #** opt1顯示數值 ’’ using ($0+0.4):($4+0.0015):4 with labels title ’’ #** opt2顯示數值 ``` 第3行後的using前都有個空白的單引號,代表同上的檔案 * with histogram -> 以長條圖做圖,xtic -> x軸 tick label 第一個數字是取第幾個column作為資料,xtic(1)代表取哪個column當作X軸標記,所以是取1,也就是 append()跟findName() ,title ’optimized1’ -> 資料標題 `** with labels` ->做標籤,第一個參數和第二個參數為座標位置,第三個參數為要印出的字串,也就是資料本身, $2-4 -> 一樣是第某column的資料,只用在用算時,區別常數與參數, title ’ ’ = notitle -> 不需標題 把印數字都放到最後是因為怕圖形把數字蓋掉了,因為gnuplot也是一行一行讀下來,所以後執行的會把先執行的蓋掉 `#` 為註解,但是不可以在 \ 後面,\ 後面連空白都不能有(上面只是做說明多加的) 參考資料 與同學 黃鏡清 王紹華 共同討論 GRUB2中文指南第二版 assert manual gnuplot.source 老師的案例分析 ```shell # lscpu # cat /sys/devices/system/cpu/cpu0/cache/index*/size //cache size # cat /sys/devices/system/cpu/cpu0/cache/index*/coherency_line_size //cache_line size (cache最小單位) ``` http://enginechang.logdown.com/posts/249025-discussion-on-memory-cache