2018q1 Homework1 (作業區) ====================== contributed by <`workat60474`> ### Reviewed by `rex662624` * 記得在 Hackmd 加上 “contributed by” * 問題回答的最後一題,有說到可以分析名字中哪個部分重複最少,但這將會使worst case非常的耗時,可能要把first name、last name等等都分析過才知道哪個部分最少重複。 * Hash 中 Collision 用到的不是這次作業較常見的 Chaining 而是 Open addressing ,因此也可以更詳細解釋讓大家了解,或是跟 Chaining 做效能上的比較。 ----- * 系統環境架構 * Cache * 將Cache的特性應用在程式上 * 實驗1.減小 size of entry * 實驗2 利用hash來加速 * 實驗3 利用Binary search tree 來加速 系統環境 ------ Ubuntu 16.04.2 :::danger 請更新 Ubuntu Linux 到 17.04 後的版本,否則後續的作業可能會無法正確安裝環境 :notes: jserv ::: Linux version 4.8.0-36-generic gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) * Architecture: x86_64 * CPU 作業模式: 32-bit, 64-bit * Byte Order: Little Endian * CPU(s): 4 * On-line CPU(s) list: 0-3 * 每核心執行緒數:2 * 每通訊端核心數:2 * Socket(s): 1 * NUMA 節點: 1 * 供應商識別號: GenuineIntel * CPU 家族: 6 * 型號: 61 * Model name: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz * 製程: 4 * CPU MHz: 1008.544 * CPU max MHz: 3100.0000 * CPU min MHz: 500.0000 * BogoMIPS: 5400.35 * 虛擬: VT-x * L1d 快取: 32K * L1i 快取: 32K * L2 快取: 256K * L3 快取: 3072K * NUMA node0 CPU(s): 0-3 ![](https://i.imgur.com/LsrOa9P.png) ## Cache 因為不同的記憶體技術其存取速度和價錢上的差異,現代電腦架構利用各自的優點來建構 階層式記憶體( Memory hierarchy),在此架構中,越是靠近cpu的記憶體其存取速度越快, 但價格也越高,相反地,越是在下面階層的記憶體,存取速度較慢,價格也較為便宜,如下圖所示。 ![「memory hierarchy」的圖片搜尋結果](https://m.eet.com/images/eetimes/2017/01/1331205/Besang01.png) 而最接近CPU的記憶體稱為Cache(快取),最下層則為Disk,介於Cache和Disk之間的稱為Main memory。 ![「memory hierarchy」的圖片搜尋結果](http://ece-research.unm.edu/jimp/611/slides/chap5_1-1.gif) ## 將Cache的特性應用在程式上 **Locality:** 首先,由於程式在一小段時間只會存取一小部分的address space,這樣的行為稱為**locality(區域性)**,locality可分為兩種不同的類型 1. spatial locality (空間區域性):意指如果一個entry被存取到,那麼他的位址附近的entry也會很快被存取到。 2. temperal locality (時間區域性):意指如果一個entry被存取到,那麼它很快地會再次被存取到。 **Cache miss**: cache miss:當資料無法在快取記憶體中取得即稱為cache miss,cache misses可以分成以下3種種類 1. compulsory misses(強迫性失誤):當這個區塊從未被使用過,那麼在它第一次被讀取的時候,此記憶體區塊必定不存在於快取中,這種情況下第一次存取此區塊就會發生失誤。 2. capacity misses(容量性失誤):程式在執行時,若快取記憶體的容量不足以包含所有該程式需要讀取的區塊,就會發生快取失誤,而其中,capacity misses發生的原因在於,cache block不斷的被置換,而短暫時間後又被重新置換。 3. conflict misses(衝突性失誤):在set-associative cache 中或者direct mapping cache中,當多個block競爭同一個working set,但是同一份working set不能同時出現在不同的block中。 confict misses只會發生在 direct mapping 或者 set-associative cache中,在fully associative的快取中是不會發生的。 熟悉了以上幾點,開始來探討如何寫出cache friendly code? 先從簡單的矩陣乘法看起: ![](https://i.imgur.com/3cNpb38.png) 並且假設 * Matrix element為double type (佔8個byte) * 矩陣為 n*n大小 * 一個cache block 佔 32bytes (4個double) * Cache size C遠小於n 如果我們採用非常基本的矩陣相乘程式碼 ``` for (i=0;i<n;i++) for(j=0;j<n;j++) for(k=0;k<n;k++) c[i][j]+=a[i][k]*b[k][j]; ``` 那麼在第一個iteration中($c[0][0]=\sum_{i=0}^{n}a[0][i]*b[i][0]$) ![](https://i.imgur.com/jOJLTZd.png) (圖中,紅色部分的entry代表發生 cache miss 的entry,而淡黃色部分的entry,因為先前發生了cache miss而一同被置換進cache的部分,代表cache hit。) 會產生的 cache misses次數為: $\frac{n}{4}+n=\frac{5}{4}n$ * 其中的 $\frac{n}{4}$ 是因為矩陣A的 $Row 0$ 由於起初不存在於 cache 中,會發生 cache miss,因為電腦架構是row-major,所以單就 $Row 0$ 共會發生 $\frac{n}{4}$次cache misses。 * 而 $n$ 則是因為,對矩陣B來說,因為要 access B的$Col0$中的所有element,而在每次的 access,而又因為row-major的關係,除了會被使用的$b[j][0]$這個element,其他被一起置換進來的如 $b[j][1], b[j][2] ...b[j][n]$, 都不會被用到,而當要access B的$b[j+1][0]$時又會再度發生一次cache misses , 這導致了一共 會發生 $n$ 次的cache misses。 因為矩陣具有$n^{2}$個entries,每次發生 $\frac{5}{4}n$ 次cache misses,一共會發生$n^{2}*\frac{5}{4}n=\frac{5}{4}n^{3}$ 次cache misses。 如何優化?考慮block matrix 乘法 ![](https://i.imgur.com/EtXbnw9.png) 想法非常簡單,與其一次計算C的一個row,不如一次計算C中的一個block(如圖中的橘色部份),要能夠計算C中的block,會需要矩陣A和矩陣B的淡黃色部分,這正好是一個cache block能夠放進的資料大小! 在一個iteration中,cache miss的次數大幅減少了! 不僅如此,當我們考慮計算下一個C中的block時 ![](https://i.imgur.com/7vTVNsb.png) 只需要透過重新放置B的下一個block就可以了。 不過,假設block的size為$B^2$個double,在挑選block的size時,需要注意的是cache至少要能夠存放3個block的大小,也就是A,B,C正在計算的3個block。 $=> 3B^{2}<<C$ 而每次計算C中的一個block會有 $(\frac{B}{4}*B)*\frac{2n}{B}=\frac{B^{2}}{4}*\frac{2n}{B}=\frac{nB}{2}$次cache misses 而一共需要計算$(\frac{n}{B})^{2}$個blocks 所以透過block來進行乘法的想法,總共cache miss的次數為: $\frac{n^{3}}{2B}$ 和原本的$\frac{5n^{3}}{4}$次cache misses比較起來是不是快了不少呢?! 結論: 1. 要對code做cache最佳化是一件“platform specific"的任務。(依據cache size,line size,關聯度等數值有緊密關聯) 2. 盡可能保持working set的最小化。(因為temporal locality) 3. 選用small stride(因為spatial locality) ## 實驗1 減小 size of entry 想法非常簡單,就像作業解說影片裡面提到的,由於原本的 entry size 為 136 bytes,但是我們只透過比對last name欄位來進行比對,若把entry size縮小,並且另外定義addi_entry來存放其他的欄位,就能降低entry size到36 bytes。 phonebook_orig.h ``` /* original version */ 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; ``` ``` size of entry : 136 bytes execution time of append() : 0.049628 sec execution time of findName() : 0.005275 sec ``` Cache miss 在orig的版本高達93% ``` Performance counter stats for './phonebook_orig' (100 runs): 3,415,271 cache-misses # 93.074 % of all cache refs ( +- 0.04% ) 3,669,415 cache-references ( +- 0.16% ) 261,936,549 instructions # 1.43 insn per cycle ( +- 0.02% ) 183,523,356 cycles ( +- 0.34% ) 0.060063780 seconds time elapsed ( +- 0.39% ) ``` phonebook_opt.h ``` typedef struct addi_entry{ char firstName[16] ; char email[16]; char phone[10]; char cell[10]; char addr1[16]; char addr2[16]; char city[16]; char stat[2]; char zip[5]; }detail_entry; 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]; detail_entry *detail ; struct __PHONE_BOOK_ENTRY *pNext; } entry; ``` ``` size of entry : 32 bytes execution time of append() : 0.035499 sec execution time of findName() : 0.002242 sec ``` Cache-misses在opt(降低了entry size)的版本為72% ``` 1,190,529 cache-misses # 72.689 % of all cache refs ( +- 0.08% ) 1,637,849 cache-references ( +- 0.32% ) 244,368,274 instructions # 1.98 insn per cycle ( +- 0.02% ) 123,133,474 cycles ( +- 0.35% ) 0.040567132 seconds time elapsed ( +- 0.47% ) ``` ![](https://i.imgur.com/acoBukq.png) :::danger 你要如何解釋上圖呈現的時間落差?請量化分析。 :notes: jserv ::: **為何cache miss降低了?** 由於我的 L1d Cache之大小為32kb,因此在orig的版本中,因為entry的大小136bytes,L1d cache能夠存放的entry數目為(32 * 1024)/136=240筆struct資料,而之後縮小了entry大小至36bytes,L1d cache能夠存放的資料數目為(32* 1024)/36=910筆,能夠存放的資料增加了,cpu有更多次可以在cache裡面找到所需的資料,這是造成cache miss降低的原因。 **為何append和findname的時間降低了?** 當發生cache miss的時候,cpu會 1.stall cpu ,凍結暫存器裡面的資料。 2.使用分離的cache controller 來處理cache miss,並透過置換演算法(如LRU,FIFO),選出要放置資料的cache,並將資料從main memory載入到cache。 3.一旦資料載入cache,我們將從造成cache miss的cycle重新開始執行。 在Memory hierarchy的架構下,不同level之記憶體之存取速度不同,在步驟2中cpu到cache的下一層中去尋找資料,所需的access time(或說access latencies) 比起cache差異慎大(見下圖) ![latency](http://cenalulu.github.io/images/linux/cache_line/latency.png) 透過降低cache miss rate,就能減少存取main memory的次數,也就能減少miss penalty的時間,這是append和findname的時間降低的原因。 ———————————————————————————————————————————— *遇到的小問題:我在減少entry大小之後,使用make plot所畫出來的圖卻和perf驗證的不同, 在圖上append和findname的時間都沒有降低!後來仔細看了一下問題出在 一開始的phonebook_opt.h裡面的 ``` //#define opt 1 ``` 是被註解的,這導致了phonebook資料夾下根本不會生成opt.txt,因為在main.c裡面有這麼一段 ``` #ifdef OPT #define OUT_FILE "opt.txt" #else #define OUT_FILE "orig.txt" #endif ``` 這導致了它只會生成orig.txt,所以只要把phonebook_opt.h裡面的 ``` //#define opt 1 ``` 把註解拿掉改成 ``` #define opt 1 ``` 就能成功生成opt.txt,如此一來make plot所畫出的圖才會符合perf的結果。 ## 實驗2 利用hash來加速 設定hash_table size為32768 * Based on BKDR hash funtion 1,756,771 cache-misses # 78.296 % of all cache refs ( +- 0.11% ) 2,243,769 cache-references ( +- 0.38% ) 272,646,864 instructions # 1.32 insn per cycle ( +- 0.02% ) 205,835,785 cycles ( +- 0.34% ) 0.068301698 seconds time elapsed ( +- 0.35% ) ``` size of entry : 24 bytes execution time of append() : 0.041564 sec execution time of findName() : 0.000000 sec ``` ![](https://i.imgur.com/QxrBhHB.png) 如圖,可以看出append的時間增加,findName的時間大幅降低,Cache misses的比例為78%。 WHY? * findName的時間大幅降低是可以想見的,由於先前的version-1採取linked-list來存放資料,在linked-list中找到特定欄位所需之time complexity為$O(n)$,而hash table中存取特定欄位,考慮collision和Probing的發生,其complexity為$O(1)(amortized)$ ,比對的時間大幅減少(為constant time)。 * append所花的時間增加:利用perf發現花在計算hash_index的時間為hot spot,append的時間甚至少他的一半,下面附上其程式碼。 * 利用perf查看overhead發現許多時間花費在計算hash index上。 ``` Samples: 308 of event 'cpu-clock', Event count (approx.): 77000000 Overhead Command Shared Object Symbol 33.12% phonebook_hash phonebook_hash [.] release_hash_table 10.39% phonebook_hash phonebook_hash [.] main 8.12% phonebook_hash libc-2.23.so [.] _int_malloc 6.49% phonebook_hash libc-2.23.so [.] free 5.84% phonebook_hash phonebook_hash [.] bkdr_hash 5.52% phonebook_hash libc-2.23.so [.] _IO_fgets ``` ``` unsigned int bkdr_hash(char *str) { unsigned seed = 127; unsigned hash=0; while(*str) hash = hash*seed+(*str++); return hash % HASH_TABLE_SIZE; } ``` **如何降低cache misses?** 起初我將hash table的size設為32768,嘗試改變hash table size的大小來觀察對於cache misses的影響 1. hash table size = 42737 ``` 1,463,096 cache-misses # 64.478 % of all cache refs ( +- 1.07% ) (25.49%) 2,269,125 cache-references ( +- 1.28% ) (26.34%) 274,351,833 instructions # 1.18 insn per cycle ( +- 1.11% ) (25.79%) 233,485,406 cycles ( +- 0.36% ) (25.32%) 0.081729874 seconds time elapsed ( +- 0.37% ) ``` cache misses 確實有下降,我後續嘗試了不同的數值: | hash table size | cache misses | cache reference|append time| | -------- | -------- | -------- |-------| | 10000|3505174| 5186209|0.047050sec| | 20000|1506168|2101795|0.050490sec | 30000|1450283|2191374|0.050414sec | 40000|1514148|2244696|0.045116sec | 50000|1495635|2293420|0.049817sec | 60000|1543214|2381301|0.046142sec | 70000|1608300|2441266|0.047791sec | 80000|1418144|2316503|0.046550sec | 90000|1578929|2424464|0.054449sec | 100000|1327630|2431227|0.047374sec | 110000|1335383|2421136|0.047381sec | 120000|1401031|2495336|0.051134sec | 130000|1625883|2564941|0.046979sec | 140000|1370780|2283537|0.051699sec | 160000|1384432|2321349|0.046852sec Q:為什麼hash table size 在10000以下,會造成cache misses上升? A:由於這次實驗採open addressing ,一旦發生collision,得透過Probing到其他的buckets去尋找空的slot來存放資料。 考慮在hash table size遠大於cache size的前提下(本例): 當hash table size減少時會造成load factor 的上升,代表整個hash table已經被用以存放資料的比例越高,也就是說空的slot數目較少,這也造成需要更多次的Probing來尋找空的slot,違反locality of reference,Cache misses也隨之提高。 換言之,collision的次數和cache miss的次數為正相關。 反之若是hash table size小於cache size,那麼整個 hash table 的資料都能夠存放在cache中,並不會有因為拜訪hash table中某個slot的資料造成cache miss的情況。 cache misses在hash table size大於20000以後,並無明顯差異,append()time也無明顯差異。 ## 實驗3 利用Binary search tree 來加速 參考了[Ryan Wang同學的作法](https://hackmd.io/s/Sk-qaWiYl#) 利用Sorted Linked List to Balanced BST 作法請參考:https://www.geeksforgeeks.org/sorted-linked-list-to-balanced-bst/ ``` example: Input: Linked List 1->2->3 Output: A Balanced BST 2 / \ 1 3 Input: Linked List 1->2->3->4->5->6->7 Output: A Balanced BST 4 / \ 2 6 / \ / \ 1 3 4 7 ``` 探討其操作的複雜度 使用Binary search,insert/search/deletion之time-complexity取決於find的時間複雜度,也就是樹的高度。 因此對一個具有$N$個elements的binary search tree來說: | $time-complexity$ | Best-case | average-case |worst-case | | -------- | -------- | -------- | --------| | insertion| $O(logN)$| $O(logN)$ |$O(N)$| | find-specify-element| $O(logN)$| $O(logN)$ |$O(N)$| 實驗結果: ``` size of entry : 32 bytes execution time of append() : 0.041623 sec execution time of findName() : 0.000000 sec 1,589,273 cache-misses # 81.076 % of all cache refs ( +- 0.09% ) 1,960,233 cache-references ( +- 0.24% ) 343,192,582 instructions # 1.53 insn per cycle ( +- 0.01% ) 224,503,303 cycles ( +- 0.18% ) 0.073416944 seconds time elapsed ( +- 0.21% ) ``` ![](https://i.imgur.com/UGyLSpR.png) | | orig | reduece-entry-size | Hash | B.S.T| | -------- | -------- | -------| -------- | -------- | | append-time| 0.052535 sec | 0.052535 sec | 0.062574 sec | 0.036677 sec| | findName-time | 0.005210 sec | 0.001943 sec |0.000000 sec| 0.000000 sec| | cache-miss | 3,415,271 | 1,190,529 |1,463,096|1,589,273 | |instruction-count | 261,936,549 |244,368,274 |274,351,833| 343,192,582| * Cache-misses增加的原因: 在binary-search-tree中,由於樹狀結構,每次發生cache-misses時,cache-controller將main memory中的資料搬到cache,因為Tree structure無法如同array或者linked-list的排列方式,無法發揮cache中的locality of reference的效益。 * findName比起reduce_entry_size時間低的原因: B.S.T search的複雜度為$O(logN)$~$O(N)$,而由於原本的資料是已經排序過的, 如此建成的B.S.T不會為worst-case,考慮average-case的話: $N$$\approx$$400000$ linked-list search的time-complexity $O(N)$ 而B.S.T average的search time-complexity $O(logN)$ $\frac{N}{logN}\approx$ $21494.2357731$ 倍的時間差距 * append時間比起reduce_entry_size時間高的原因: 由於B.S.T利用insertion來建樹,所花的time-complexity為$O(NlogN)$,相較於link-list建立的time-complexity為$O({N}^2)$, $\frac{{N}^2}{NlogN}\approx$ $18$ * 為什麼時間和複雜度所計算出的比例不同? ## 問題回答 補充: 本例選用的 dataset (也就是輸入電話簿的姓名) 是否存在問題? * 有代表性嗎?跟真實英文姓氏差異大嗎? ANS:差異慎大,許多姓名不存在於現實世界中,不僅如此,只透過last name來進行批配也不夠嚴謹, 應該考慮last name重複的話,再對於姓名的其他部份(eg. middle name,first name) 再進行比較。 * 資料難道「數大就是美」嗎? ANS:與其說數大便是美,不如說能夠使用適當且有效率的資料結構和演算法,來降低對資料操作的複雜度, 並了解資料的特性,搭配電腦硬體的適應方式,寫出適當且有效率的程式碼。 * 如果我們要建立符合真實電話簿情境的輸入,該怎麼作? ANS: 考慮到姓名中,可能重複的部份,如果能夠先對資料整體做一次分析,判斷中先利用名子的哪個部份做批配,發生重複的機率最低, 再搭配適當資料結構和演算法來撰寫存放資料和查找的方法。 ## 參考資料 * [关于CPU Cache -- 程序猿需要知道的那些事](http://cenalulu.github.io/linux/all-about-cpu-cache/) * [淺談memory cache ](http://opass.logdown.com/posts/249025-discussion-on-memory-cache) * https://zh.wikipedia.org/wiki/%E5%93%88%E5%B8%8C%E8%A1%A8#.E8.BD.BD.E8.8D.B7.E5.9B.A0.E5.AD.90