contributed by <F74021200
>
中英文字間請以空白隔開
進度落後太多請快快追上!
課程助教
好的!
F74021200
電腦規格:
tinin@tinin:~$ lscpu
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
型號: 58
Model name: Intel(R) Core(TM) i5-3230M CPU @ 2.60GHz
製程: 9
CPU MHz: 2994.030
CPU max MHz: 3200.0000
CPU min MHz: 1200.0000
BogoMIPS: 5188.16
虛擬: VT-x
L1d 快取: 32K
L1i 快取: 32K
L2 快取: 256K
L3 快取: 3072K
NUMA node0 CPU(s): 0-3
linux kernel:
tinin@tinin:~$ uname -a
Linux tinin 4.8.0-39-generic
#42~16.04.1-Ubuntu SMP Mon Feb 20 15:06:07 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$./phonebook_orig & sudo perf top -p $!
39.16% libc-2.23.so [.] __strcasecmp_l_avx
31.70% phonebook_orig [.] findName
7.24% [kernel] [k] release_pages
5.56% phonebook_orig [.] strcasecmp@plt
5.45% [kernel] [k] unmap_page_range
3.60% [kernel] [k] nmi
1.83% libc-2.23.so [.] __strcasecmp_avx
1.82% [kernel] [k] common_perm_cond
1.82% [kernel] [k] mem_cgroup_update_lru_size
1.81% [kernel] [k] kmem_cache_free
0.01% [kernel] [k] native_iret
0.00% [kernel] [k] native_write_msr
消耗 CPU 周期最多的部份為函式 findName()
$ ./phonebook_orig
size of entry : 136 bytes
execution time of append() : 0.048713 sec
execution time of findName() : 0.005059 sec
sudo sh -c " echo 0 > /proc/sys/kernel/kptr_restrict"
執行
perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./phonebook_orig
Performance counter stats for './phonebook_orig' (100 runs):
2,096,128 cache-misses # 94.483 % of all cache refs ( +- 0.20% )
2,218,535 cache-references ( +- 0.16% )
262,467,890 instructions # 1.32 insn per cycle ( +- 0.02% )
199,293,309 cycles ( +- 0.57% )
0.065489355 seconds time elapsed ( +- 0.69% )
由上圖知, cache miss 高達 94.800%
檢視程式與資料結構發現,在搜尋 phonebook 時,僅使用到 lastName ,因此,透過縮小 struct,使能加入 cache 的資料 struct 增加,從而降低 cache miss 發生的機率。
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;
修改後
$ ./phonebook_opt
size of entry : 24 bytes
execution time of append() : 0.044088 sec
execution time of findName() : 0.002203 sec
執行時間縮短了!
perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./phonebook_opt
Performance counter stats for './phonebook_opt' (100 runs):
253,406 cache-misses # 71.468 % of all cache refs ( +- 0.26% )
354,574 cache-references ( +- 0.93% )
241,179,130 instructions # 1.80 insn per cycle ( +- 0.02% )
133,748,026 cycles ( +- 0.67% )
0.043806218 seconds time elapsed ( +- 0.69% )
由上圖知, cache miss 發生次數下降了
使用 hash function 增加搜尋速度
In phonebook_opt.h
struct phonebook {
Entry *Data[HASH_TABLE_SIZE];
func_t findName;
read_t readFile;
};
In phonebook_opt.c
static Entry *findName_impl(char lastName[], Phonebook **self)
{
Entry *e;
e = ((*self)->Data)[BKDRHash(lastName) % HASH_TABLE_SIZE];
while (e) {
if (strcasecmp(lastName, e->lastName) == 0)
return e;
e = e->pNext;
}
return NULL;
}
static void readFile_impl(FILE *fp, Phonebook **self)
{
char line[MAX_LAST_NAME_SIZE];
int i = 0;
Entry *e;
while (fgets(line, sizeof(line), fp)) {
while (line[i] != '\0')
i++;
line[i - 1] = '\0';
i = 0;
e = (*self)->Data[BKDRHash(line) % HASH_TABLE_SIZE];
while(e->pNext)
e = e->pNext;
e = append(line, e);
}
}
Entry *append(char lastName[], Entry *e)
{
e->pNext = (Entry *) malloc(sizeof(Entry));
e = e->pNext;
strcpy(e->lastName, lastName);
e->pNext = NULL;
return e;
}
int init_phonebook (Phonebook **self)
{
int i;
if (NULL == ((*self) = malloc(sizeof(Phonebook)))) return -1;
for (i = 0; i < HASH_TABLE_SIZE; ++i) {
if (NULL == (((*self)->Data)[i] = (Entry *) malloc(sizeof(Entry)))) return -1;
((*self)->Data)[i]->pNext = NULL;
}
(*self)->findName = findName_impl;
(*self)->readFile = readFile_impl;
return 0;
}
unsigned int BKDRHash(char *str)
{
unsigned int seed = 131;
unsigned int hash = 0;
while (*str) {
hash = hash * seed + (*str++);
}
return hash;
}
修改後
$ ./phonebook_opt
execution time of append() : 30.423652 sec
execution time of findName() : 0.000126 sec
perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./phonebook_opt
Performance counter stats for './phonebook_opt' (100 runs):
294,807,802 cache-misses # 65.948 % of all cache refs ( +- 0.68% )
447,031,194 cache-references ( +- 0.07% )
3,394,511,333 instructions # 0.04 insn per cycle ( +- 0.01% )
80,193,329,008 cycles ( +- 0.57% )
27.472831615 seconds time elapsed ( +- 0.50% )
搜尋時間確實下降了(0.005059 sec->0.000126 sec), cache-miss 也下降(94.483%->65.948 %),但讀入時間增加太多(0.044088 sec->30.423652 sec),應該是因為在讀入每筆資料時,都須先計算其 hash 值,在計算 hash 值時,除了額外計算的時間,還有呼叫函式的時間,另外,當找到 hash 值後,若所在的陣列已有其他資料,又必須經過一串 linked list 到達最後一個後,才能將新資料加入;因此,讀入所有資料的時間才會如此可觀。
以下嘗試兩種方法:
static Entry *findName_impl(char lastName[], Phonebook **self)
{
Entry *e;
unsigned int seed = 131;
unsigned int hash = 0;
char *str;
str = lastName;
while (*str) {
hash = hash * seed + (*str++);
}
e = ((*self)->Data)[hash % HASH_TABLE_SIZE];
while (e) {
if (strcasecmp(lastName, e->lastName) == 0)
return e;
e = e->pNext;
}
return NULL;
}
static void readFile_impl(FILE *fp, Phonebook **self)
{
char line[MAX_LAST_NAME_SIZE], *str;
int i = 0;
unsigned int seed = 131;
unsigned int hash = 0;
Entry *e;
while (fgets(line, sizeof(line), fp)) {
while (line[i] != '\0')
i++;
line[i - 1] = '\0';
i = 0;
str = line;
hash = 0;
while(*str) {
hash = hash * seed + (*str++);
}
e = (*self)->Data[hash % HASH_TABLE_SIZE];
while(e->pNext)
e = e->pNext;
e = append(line, e);
}
}
$ make run
(上為原本的 hash 版本,下為修改後的)
execution time of append() : 33.082860 sec
execution time of findName() : 0.000200 sec
execution time of append() : 33.490506 sec
execution time of findName() : 0.000144 sec
$ perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./phonebook_opt_1
Performance counter stats for './phonebook_opt_1' (100 runs):
332,570,144 cache-misses # 73.024 % of all cache refs ( +- 0.75% )
455,426,921 cache-references ( +- 0.16% )
3,394,695,626 instructions # 0.04 insn per cycle ( +- 0.02% )
88,919,125,151 cycles ( +- 0.70% )
29.833768554 seconds time elapsed ( +- 0.51% )
結果,並沒有獲得太大的改善。
說明:因為原本的情況是,在加入一筆資料時,會將其放在所屬 hash set 裡 linked-list 的最後一個;現在,改將其直接加入到第一個位置。
static void readFile_impl(FILE *fp, Phonebook **self)
{
char line[MAX_LAST_NAME_SIZE], *str;
int i = 0;
unsigned int seed = 131;
unsigned int hash = 0;
Entry *e, *e1;
while (fgets(line, sizeof(line), fp)) {
while (line[i] != '\0')
i++;
line[i - 1] = '\0';
i = 0;
str = line;
hash = 0;
while(*str) {
hash = hash * seed + (*str++);
}
e = (*self)->Data[hash % HASH_TABLE_SIZE];
e1 = e->pNext;
e->pNext = NULL;
e = append(line, e);
e->pNext = e1;
}
}
$ make run
execution time of append() : 31.395222 sec
execution time of findName() : 0.000202 sec
execution time of append() : 0.092144 sec
execution time of findName() : 0.000000 sec
$ perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./phonebook_opt_1
Performance counter stats for './phonebook_opt_1' (100 runs):
260,094 cache-misses # 59.930 % of all cache refs ( +- 0.20% )
433,995 cache-references ( +- 1.34% )
231,826,919 instructions # 1.43 insn per cycle ( +- 0.02% )
161,765,903 cycles ( +- 1.40% )
0.057799116 seconds time elapsed ( +- 1.44% )
輸入時所花時間大幅下降,由此可知,在原版本的輸入中,於 linked-list 的搜尋花了很多時間(上為原本的 hash 版本,下為修改後的);另外,修改後,搜尋字串"zyxel"的時間降為0.000000 sec ,在檢查 words.txt 後,發現"zyxel"為所在 hash set 的第一個結點,可能因此使得搜尋時間非常短。
使用不同的資料結構觀察效能的改變,這次用 binary search tree 。
In phonebook_bst.h
struct __PHONE_BOOK_ENTRY {
char lastName[MAX_LAST_NAME_SIZE];
struct __PHONE_BOOK_ENTRY *left;
struct __PHONE_BOOK_ENTRY *right;
};
struct phonebook {
Entry *Data;
func_t findName;
read_t readFile;
};
In phonebook_bst.c
static Entry *findName_impl(char lastName[], Phonebook **self)
{
Entry * p;
int cmp;
p = (*self)->Data;
while (1) {
cmp = strcmp (lastName, p->lastName);
if (cmp > 0) {
if (p->right)p = p->right;
else {
printf ("No such name!\n");
return NULL;
}
}
else if (cmp < 0) {
if (p->left)p = p->left;
else {
printf ("No such name!\n");
return NULL;
}
}
else {
return p;
}
}
}
static void readFile_impl(FILE *fp, Phonebook **self)
{
int i = 0;
char line[MAX_LAST_NAME_SIZE];
while (fgets(line, sizeof(line), fp)) {
while (line[i] != '\0')
i++;
line[i - 1] = '\0';
i = 0;
insert (line, (*self)->Data);
}
}
Entry *insert(char lastName[], Entry *Data)
{
Entry *p;
int cmp;
p = Data;
while (1) {
cmp = strcmp (lastName, p->lastName);
if (cmp > 0) {
if (p->right)p = p->right;
else {
p->right = (Entry *) malloc (sizeof(Entry));
p = p->right;
strcpy (p->lastName, lastName);
p->left = NULL;
p->right = NULL;
return p;
}
}
else {
if (p->left)p = p->left;
else {
p->left = (Entry *) malloc (sizeof(Entry));
p = p->left;
strcpy (p->lastName, lastName);
p->left = NULL;
p->right = NULL;
return p;
}
}
}
}
int init_phonebook (Phonebook **self)
{
if (NULL == (*self = malloc(sizeof(Phonebook)))) return -1;
if (NULL == ((*self)->Data = (Entry *) malloc(sizeof(Entry)))) return -1;
strcpy((*self)->Data->lastName, "n");
(*self)->Data->left = NULL;
(*self)->Data->right = NULL;
(*self)->findName = findName_impl;
(*self)->readFile = readFile_impl;
return 0;
}
在觀察 words.txt 後,我發現資料的前幾個字母是按照 abcdefg … 的方式排列,為了不使 tree 中的結點都集中在某一邊,我先將根結點預設為 n 。
$ ./phonebook_bst
execution time of append() : 195.034462 sec
execution time of findName() : 0.000834 sec
與 orig 相比,搜尋時間確實減少了,但讀入資料的時間卻太長;
與 hash function 版本相比,除了搜尋時間較長外,讀入時間亦長的誇張,由 words.txt 中的資料來看,應是所產生的 tree 太過類似 skewed tree 所導致的。
不過, cache-miss 卻下降許多(94.483%->41.123 %),如下:
Performance counter stats for './phonebook_bst' (100 runs):
1,908,252,577 cache-misses # 41.123 % of all cache refs ( +- 1.27% )
4,640,306,720 cache-references ( +- 1.10% )
1,167,592,842,608 instructions # 2.26 insn per cycle ( +- 0.22% )
516,320,047,658 cycles ( +- 0.47% )
163.680049478 seconds time elapsed ( +- 0.54% )
問題:
Q1.:本例選用的dataset(也就是輸入電話簿的姓名)是否存在問題?
A1.:
首先,須確認dataset 中資料有多少比例是目前真實存在的英文姓氏,因為在電話簿中,不可能出現非姓氏的字,若出現,則此資料中的人也不存在,這筆資料會被直接捨棄,因此,當此 dataset 中出現非姓氏詞時,就表示此 dataset 有問題;所以,要回答這問題需要對此 dataset 中的資料與真實姓氏的資料比對,依比對結果回答。
Q2.:有代表性嗎?跟真實英文姓氏差異大嗎?
A2.:
關於是否有代表性,需要考慮許多因素,就此電話簿所用年代而言,就必須考慮那些姓氏出現在所用年代,有一 網站 能查詢到英文姓氏在不同年代於美國的分布以及其起源,舉個例,依記載,在1843到1999年,紀錄了一位名叫 Elka Aaaa 的人,確實有人的姓氏是 aaaa;所以,需要知道更多此電話簿的用途資訊,才能知道其是否有代表性;關於第二個問題,可能需要先更明確的定義'所要的真實英文姓氏'的定義,才能有後續的動作,就如上一個問題中一樣,所謂'真實英文姓氏'也有年代問題,不同年代的英文姓氏組成不同,須先設定好年代才能比較差異性。
Q3.:資料難道「數大便是美」嗎?
A3.:
就統計學來說,於母群體中,隨機抽樣的數量越多,資料分析出的結果越趨近於母群體,因此,'數大',只代表取樣的數量大,第一,沒限制須隨機抽樣,這很可能導致資料出現極大偏差;第二,沒限制所取樣本是在母群體中,這會使得資料的代表性降低;因此,資料並非'數大'便是美,所謂 garbage in, garbage out ,即使'數大'的垃圾,所產亦是垃圾。
Q4.:如果我們要建立符合真實電話簿情境的輸入,該怎麼作?
A4.:
首先,要先確認此情境的設定,例如:年代、地區,若情況允許,當然是直接取得該地的人口姓氏,若無法取得,但時間允許的話,就在此地區隨機分配調查人員,依照人群流動時的疏密程度比例配置調查員的數量,如此能盡可能將母群體中的資料都蒐集到,使取樣資料越接近母群體的實際資料;若無法做實地調查,則無法知道各姓氏所佔比例,那只好從現在仍存在的所有姓氏中,隨機取樣做出電話簿了。
這種等級的回覆,跟文組學生有什麼差別?理工人要拿出數據、數學模型,還有推理出來! –jserv
是!我趕緊修改!
F74021200
參考資料:
perf 原理和實務
Programming Small
gnuplot 語法解說和示範
Makefile 語法和示範
hash function 觀念和實務
哈西表
哈西函數
哈西函式 in C
你所不知道的 C 語言:物件導向程式設計篇
How to Write a Git Commit Message
Hsiang的共筆
by F74021200