contributed by <raygoah
>
afcidk
findName()
看不太出來效能的差別,是否有嘗試過查詢不同名字或是多次查詢來進行比較?LinYunWen
HASH_TABLE_LENGTH = 1024
rwe0214
O(n)
,而不是 O(nlogn)
,如果是 O(nlogn)
時間應該不只這樣><這裡對應的程式碼修改應一併傳到 GitHub 上
commit message 的撰寫請參閱【譯】如何撰寫Git提交訊息
vvn好的,謝謝提醒 raygoah
$ uname -a
Linux raygoah-X450JN 4.13.0-32-generic #35~16.04.1-Ubuntu SMP Thu Jan 25 10:13:43 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 60
Model name: Intel(R) Core(TM) i7-4700HQ CPU @ 2.40GHz
Stepping: 3
CPU MHz: 2394.560
CPU max MHz: 3400.0000
CPU min MHz: 800.0000
BogoMIPS: 4789.12
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 6144K
NUMA node0 CPU(s): 0-7
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand lahf_lm abm cpuid_fault epb invpcid_single pti tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
中英文字間記得空白喔!vvn
感謝提醒raygoah
$ echo 1 | sudo tee /proc/sys/vm/drop_caches
$ make cache-test
size of entry : 136 bytes
execution time of append() : 0.046198 sec
execution time of findName() : 0.005292 sec
Performance counter stats for './phonebook_orig' (100 runs):
96,4703 cache-misses # 81.865 % of all cache refs ( +- 0.32% )
117,8403 cache-references ( +- 0.34% )
2,6322,4320 instructions # 1.29 insn per cycle ( +- 0.02% )
2,0401,7147 cycles ( +- 0.63% )
0.065028560 seconds time elapsed ( +- 1.07% )
$ sudo su
$ make plot
$ exit
$ sudo make plot
不需要使用 sudo XD vvn
之前沒有切換到 root ,現在學會了,已補上raygoah
想法:
main.c
後可以發現到,在 struct 中,實際會被使用到的其實只有 lastName
,推測將 struct 縮小,會降低 cache miss 的發生測試減少 entry size 是否有用:
lastName
以外的所有程式碼註解
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 後,entry size 為 24 bytes 的情況下, cache miss 的情形:
$ make cache-test
Performance counter stats for './phonebook_opt' (100 runs):
4,0009 cache-misses # 50.970 % of all cache refs ( +- 0.27% )
7,8496 cache-references ( +- 0.33% )
9230,1160 instructions # 1.70 insn per cycle ( +- 0.00% )
5416,4353 cycles ( +- 0.32% )
0.121720307 seconds time elapsed ( +- 10.95% )
可以發現到,縮小 struct 後, cache miss 發生的情形確實有改善。但因為前面使用的方法是將程式碼註解,此方法可能會影響到程式的功能,因此接著嘗試在不影響程式功能的前提下,減少 entry size
減少 entry size:
__PHONE_BOOK_ENTRY
的大小,entry size 從原本的 136 bytes 減少為 32 bytes。
typedef struct __PHONE_OTHER_ENTRY {
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];
} otherEntry;
typedef struct __PHONE_BOOK_ENTRY {
char lastName[MAX_LAST_NAME_SIZE];
struct __PHONE_BOOK_ENTRY *pNext;
otherEntry *other;
} entry;
$ make cache-test
Performance counter stats for './phonebook_opt' (100 runs):
16,1974 cache-misses # 47.797 % of all cache refs ( +- 1.18% )
33,8879 cache-references ( +- 1.04% )
2,4497,2241 instructions # 1.82 insn per cycle ( +- 0.02% )
1,3488,8711 cycles ( +- 0.48% )
0.043308698 seconds time elapsed ( +- 0.61% )
$ make plot
閱讀
在實做前遇到的插曲:
$ make
make: Nothing to be done for 'all'.
可能要先 $ make clean
再 $ make
之後每次先 $ make clean
再 $ make
後,就沒再發生過了
先把這個情況紀錄下來,要是還有出現,再來看看是哪邊出問題 raygoah
實作 hash function
依照 hash 函數比較 中介紹的各種不同的 hash function ,使用在此次作業中,並比較不同的 hash function 所得到的結果差異
unsigned int BKDRHash(char *str) {
unsigned int seed = 131;
unsigned int hash = 0;
while (*str) {
hash = hash * seed + (*str++);
}
return (hash % HASH_TABLE_LENGTH);
}
在這裡 seed 的值取 131,參考此篇,而實際將 seed 改成 13 或 267 跑跑看,結果幾乎沒差,於是選擇 131 作為 seed 值
接著觀察使用了 BKDR hash 後,cache miss 的情形
Performance counter stats for './phonebook_hash_opt' (100 runs):
8,0568 cache-misses # 38.554 % of all cache refs ( +- 0.72% )
20,8977 cache-references ( +- 1.39% )
2,3603,5839 instructions # 1.83 insn per cycle ( +- 0.02% )
1,2924,6177 cycles ( +- 0.35% )
0.043926812 seconds time elapsed ( +- 0.88% )
由上圖可以看到,cache miss 發生的比率,從之前的 47.791% 下降到 38.554%, 因此實作 BKDR hash 是有助於減少 cache miss 的發生
利用 gnuplot 繪製出圖形,觀察使用 hash 後,findName()
和 append()
所需時間有無改變,可以看到在建立了 hash table 的版本中,append()
所花的時間是有些微增加的,但在 findName()
中所花費的時間,卻是大幅的減少,在圖中甚至已經看不太到了
unsigned int APHash(char *str)
{
unsigned int hash = 0;
for (int i=0; *str; ++i) {
if ((i & 1) == 0) {
hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));
} else {
hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));
}
}
return ((hash & 0x7FFFFFFF) % HASH_TABLE_LENGTH);
}
Performance counter stats for './phonebook_hash_opt' (100 runs):
9,0705 cache-misses # 37.806 % of all cache refs ( +- 0.98% )
23,9924 cache-references ( +- 1.84% )
2,6462,1363 instructions # 1.97 insn per cycle ( +- 0.02% )
1,3420,8036 cycles ( +- 0.26% )
0.053116284 seconds time elapsed ( +- 0.88% )
append()
所花的時間也是些微增加,在 findName()
中所花費的時間,也是大幅的減少unsigned int DJBHash(char *str)
{
unsigned int hash = 5381; // magic number
while (*str)
{
hash += (hash << 5) + (*str++);
}
return ((hash & 0x7FFFFFFF) % HASH_TABLE_LENGTH);
}
Performance counter stats for './phonebook_hash_opt' (100 runs):
8,3311 cache-misses # 37.684 % of all cache refs ( +- 0.41% )
22,1080 cache-references ( +- 2.15% )
2,3564,6536 instructions # 1.82 insn per cycle ( +- 0.02% )
1,2920,6859 cycles ( +- 0.38% )
0.040968937 seconds time elapsed ( +- 0.42% )
4. 實作 SDBMHash
unsigned int SDBMHash(char *str)
{
unsigned int hash = 0;
while (*str) {
hash = hash * 65599 + (*str++);
}
return ((hash & 0x7FFFFFFF) % HASH_TABLE_LENGTH);
}
Performance counter stats for './phonebook_hash_opt' (100 runs):
9,9660 cache-misses # 40.157 % of all cache refs ( +- 0.87% )
24,8174 cache-references ( +- 1.89% )
2,3289,3548 instructions # 1.74 insn per cycle ( +- 0.02% )
1,3368,8164 cycles ( +- 0.55% )
0.042705223 seconds time elapsed ( +- 0.56% )
5. 實作 ELFHash
unsigned int ELFHash(char *str)
{
unsigned int hash = 0;
unsigned int x = 0;
while (*str) {
hash = (hash << 4) + (*str++);
if ((x = hash & 0xF0000000L) != 0) {// 28-31 is 1 others are 0
hash ^= (x >> 24);
hash &= ~x;
}
}
return ((hash & 0x7FFFFFFF) % HASH_TABLE_LENGTH);
}
Performance counter stats for './phonebook_hash_opt' (100 runs):
8,3888 cache-misses # 40.979 % of all cache refs ( +- 1.16% )
20,4710 cache-references ( +- 0.99% )
2,5497,9203 instructions # 1.84 insn per cycle ( +- 0.02% )
1,3870,1759 cycles ( +- 0.18% )
0.044650160 seconds time elapsed ( +- 0.30% )
findName()
和 append()
所需時間方面,和之前幾種 hash 相比沒什麼顯著的差異在以上幾種不同的 hash function 中,可以看到在 append()
的部份,所需花費的時間比起沒有使用 hash 的版本都有些許的增加,但在 findName()
的部份,可以很明顯的看到花費的時間大幅的減少
使用不同的 hash function,對於 cache-miss 的發生會有一點影響,在hash 函數比較 中排名較高的 hash,發生 cache-miss 的次數也有比較少的現象,但整體而言有使用 hash 比起沒有使用的版本,cache-miss rate 都是比較低的
閱讀
實作
ryanwang522
的紀錄,以及 Sorted Linked List to Balanced BST,閱讀後實作 BSTBSTNode *BST(entry **tmp_head, int length)
{
if (length <= 0)
return NULL;
BSTNode *left = BST(tmp_head, length/2);
BSTNode *root = (BSTNode *) malloc(sizeof(BSTNode));
root->entry = *tmp_head;
root->left = left;
*tmp_head = (*tmp_head)->pNext;
root->right = BST(tmp_head, length - (length / 2 + 1));
return root;
}
Performance counter stats for './phonebook_bst_opt' (100 runs):
19,0481 cache-misses # 55.459 % of all cache refs ( +- 1.04% )
34,3462 cache-references ( +- 1.15% )
2,8595,9142 instructions # 1.76 insn per cycle ( +- 0.02% )
1,6207,7716 cycles ( +- 0.62% )
0.169329355 seconds time elapsed ( +- 8.87% )
雖說整體運行時間是增加的,但從繪製的比較圖中可以看到,BST 版本中 findName()
和 append()
所需時間比起使用 hash 的版本是比較低的,如下圖所示
由於 append()
的部份是直接將 dataset 中的名字串接成 link list,所以花費的時間和縮小 struct 的 optimized
版本會是相同的,但若是把建立 BST 所需的時間加入的話,所需時間應該是會上升的,如下圖所示
而重點是在於 findName()
的部份,證實了利用 BST 在搜尋方面,時間複雜度最佳為 O(logn)
最差為 O(n)
的特性,是可以有效降低 findName()
的所需時間,但因為建立 BST 為 O(nlogn)
,所以在整體運行上所需要的時間會是變多的
從 dictionary/words.txt
中可以看到有人的姓名入下所示:
aaaa
aaaaa
aaaaaa
aaaaaaa
aaaaaaaa
aaaaaaaah
aaaaaaauugh
aaaaaagh
aaaaaahhhhh
aaaaaaugh
aaaaagh
aaaaah
很明顯的,正常來說,不會有這種由一個字母重複出現的取名方式,就算他父母很想讓他在學校永遠都當一號,而取了 aaaa
這樣的名字,但我想這也是不太可能的事情,因此我認為 dataset 中許多筆資料的和真實狀況是相差許多的,但如果這些資料是取自於網路上的 id 或是帳號暱稱,那麼就相對的就比較合理
再來是關於每筆資料都不同這點,光是一個 50 人的班級中,都很有可能出現兩個或以上名字一樣的人了,在這麼多筆資料中,每筆資料卻是不重複的,這種情況應該不太可能,當然因為 phonebook 的作業是老師經過簡化過後的情況,所以才會如此,不然一般來說必定會有重複的姓名出現,而且重複的情況應該會不少