contributed by <team6612
> (Daniel Chen)
BodomMoon
./+o+- daniel@daniel-thinkpad-ubuntu
yyyyy- -yyyyyy+ OS: Ubuntu 17.10 artful
://+//////-yyyyyyo Kernel: x86_64 Linux 4.13.0-32-generic
.++ .:/++++++/-.+sss/` CPU: Intel Core i5-5200U @ 4x 2.7GHz [47.0°C]
.:++o: /++++++++/:--:/- GPU: GeForce 940M
o:+o+:++.`..```.-/oo+++++/ RAM: 5249MiB / 7677MiB
.:+o:+o/. `+sssoo+/
.++/+:+oo+o:` /sssooo. Virtualization: VT-x
/+++//+:`oo+o /::--:. L1d cache: 32K
\+/+o+++`o++o ++////. L1i cache: 32K
.++.o+++oo+:` /dddhhh. L2 cache: 256K
.+.o+oo:. `oddhhhh+ L3 cache: 3072K
\+.++o+o``-````.:ohdhhhhh+
`:o+++ `ohhhhhhhhyo++os:
.o:`.syhhhhhhh/.oo++o`
/osyyyyyyo++ooo+++/
````` +oo+++o\:
`oo++.
Cache 詳細資訊
$ sudo dmidecode -t cache
# dmidecode 3.1
Getting SMBIOS data from sysfs.
SMBIOS 2.7 present.
Handle 0x0003, DMI type 7, 19 bytes
Cache Information
Socket Designation: L1 Cache
Configuration: Enabled, Not Socketed, Level 1
Operational Mode: Write Back
Location: Internal
Installed Size: 32 kB
Maximum Size: 32 kB
Supported SRAM Types:
Synchronous
Installed SRAM Type: Synchronous
Speed: Unknown
Error Correction Type: Parity
System Type: Data
Associativity: 8-way Set-associative
Handle 0x0005, DMI type 7, 19 bytes
Cache Information
Socket Designation: L1 Cache
Configuration: Enabled, Not Socketed, Level 1
Operational Mode: Write Back
Location: Internal
Installed Size: 32 kB
Maximum Size: 32 kB
Supported SRAM Types:
Synchronous
Installed SRAM Type: Synchronous
Speed: Unknown
Error Correction Type: Parity
System Type: Instruction
Associativity: 8-way Set-associative
Handle 0x0006, DMI type 7, 19 bytes
Cache Information
Socket Designation: L2 Cache
Configuration: Enabled, Not Socketed, Level 2
Operational Mode: Write Back
Location: Internal
Installed Size: 256 kB
Maximum Size: 256 kB
Supported SRAM Types:
Synchronous
Installed SRAM Type: Synchronous
Speed: Unknown
Error Correction Type: Single-bit ECC
System Type: Unified
Associativity: 8-way Set-associative
Handle 0x0007, DMI type 7, 19 bytes
Cache Information
Socket Designation: L3 Cache
Configuration: Enabled, Not Socketed, Level 3
Operational Mode: Write Back
Location: Internal
Installed Size: 3072 kB
Maximum Size: 3072 kB
Supported SRAM Types:
Synchronous
Installed SRAM Type: Synchronous
Speed: Unknown
Error Correction Type: Multi-bit ECC
System Type: Unified
Associativity: 12-way Set-associative
本次作業需用到 perf 進行效能分析及 gnuplot 對分析結果可視化,相關安裝及環境設定如下:
$ sudo apt update
$ sudo apt install build-essential # including g++ gcc make etc.
$ sudo apt install linux-tools-common linux-tools-general # for installing perf
$ sudo apt install astyle cppcheck colordiff gnuplot # analysis tools and code formatter
# Check your kernel release
$ uname -r
4.13.0-32-generic
# Install linux tools for your kernel release
$ sudo apt install linux-tools-4.13.0-32-generic linux-cloud-tools-4.13.0-32-generic
or
$ sudo apt install linux-tools-`uname -r` linux-cloud-tools-`uname -r`
如果未設定的話以 user 權限執行 perf top
會出現以下錯誤:
┌─Error:───────────────────────────────────────────────────────────┐
│You may not have permission to collect system-wide stats. │
│ │
│Consider tweaking /proc/sys/kernel/perf_event_paranoid, │
│which controls use of the performance events system by │
│unprivileged users (without CAP_SYS_ADMIN). │
│ │
│The current value is 3: │
│ │
│ -1: Allow use of (almost) all events by all users │
│>= 0: Disallow raw tracepoint access by users without CAP_IOC_LOCK│
│>= 1: Disallow CPU event access by users without CAP_SYS_ADMIN │
│>= 2: Disallow kernel profiling by users without CAP_SYS_ADMIN │
│ │
│To make this setting permanent, edit /etc/sysctl.conf too, e.g.: │
│ │
│ kernel.perf_event_paranoid = -1 │
│ │
│ │
│ │
│Press any key... │
└──────────────────────────────────────────────────────────────────┘
若需讓 user 直接存取 event data 的話,將 /proc/sys/kernel/perf_event_paranoid
設定爲 0
:
$ sudo sh -c "echo 0 > /proc/sys/kernel/perf_event_paranoid"
若需檢測 cache miss 則需要再將 kernel pointer 的存取權限打開:
$ sudo sh -c "echo 0 > /proc/sys/kernel/kptr_restrict"
本課程之 coding style 規範設定
$ astyle --style=kr --indent=spaces=4 --indent-switches --suffix=none *.[ch]
perf 是 linux 內建的效能分析軟體, pref 可以追蹤硬體層級及軟體層級的事件,硬體層級如 cache miss、 cpu cycle,軟體層級如 page fault、 contex switch,還有另一種事件是Kernel Tracepoint,是寫死在 linux kernel 內觸發的量測事件,如 syscalls 可記錄各種 system call 的呼叫次數,各種事件的相關整理及關係如下圖:
(Credit: Linux perf Examples,關於各種事件的詳細說明可參閱該站 5. Events)
常用指令如:
$ perf top # 即時分析各程序的 perf event 觸發狀況
$ perf stat # 對特定程序輸出指定 perf event 的統計結果
$ perf record # 可對函式級別的 event 進行統計,但不會直接輸出到終端機中,而是輸出到 perf.data
$ perf report # 讀取並顯示由 perf record 所記錄的 perf.data 檔案
本次作業關心的重點在程式在執行時的 cache miss ,因此我們來回顧一下 CPU cache 的設計及運作模式,以及 cache miss 如何發生。
程式執行時會將指令和相關的資料載入記憶體, CPU 會從記憶體中讀取指令和資料來執行,並將執行的結果存回記憶體,雖然記憶體的存取速度已經較硬碟快上許多,但仍和 CPU 的存取速度有落差,一來一往間會犧牲掉很多 CPU 的效能,我們可以把記憶體做得更快讓他能夠跟上 CPU 的存取速度,但這並不符合成本效益,於是就有 cache 的設計。
CPU cache 爲數組容量較小但速度比記憶體快上幾倍的儲存空間,供 CPU 將較常存取的資料暫存,避免頻繁與記憶體進行較慢的資料交換來提升效能,而 CPU cache 會使用較快但成本較高的 SRAM,一般的記憶體則是使用 SDRAM。(What is the difference between DRAM, SRAM, and SDRAM? Which one is the best RAM technology?)
現代的 CPU cache 爲層狀結構,分爲 L1、 L2、及 L3 cache (有些架構可能沒有 L3 cache,另外有少數的架構有做到 L4 cache),容量隨着層數遞增,速度隨着層數遞減,而 L1 cache 又分爲 L1d (data)、 L1i (instruction) 分別儲存資料及指令。
思考:為何 L1 cache 分為 L1d 和 L1i 呢?這樣的設計可帶來什麼好處?
jserv
因爲某些指令操作可能不涉及資料存取,加上 cache 是 sequential 存取,若將資料與指令分開儲存,可加速不涉及資料存取的操作。 Daniel Chen
當 CPU 在 L1 cache 中找不到特定資料時會到 L2 cache 中尋找,若還是找不到會往 L3 cache 找,最後才會到記憶體中找尋該資料,此過程稱爲 cache miss,因爲記憶體的存取會造成 latency,過於頻繁的 cache miss 會嚴重影響程式效能。
瞭解 cache 的運作模式後我們來研究看看如何從 cache 下手改進程式效能。
這是一篇 stackoverflow 上的發問,內容有關於如何在 c++ 中撰寫一個 cache-friendly 的程式碼,底下有許多的討論,其中 Marc Claesen 大大的回答中從 the principle of locality 出發,並介紹幾個可以着手的點。
jserv 老師在此講中更深入的介紹 cache line 的運作原理與 locality 等議題,以及探討如和利用 prefetch、 locality hint 等技術改善 cache 效率。
其中得到一個很不錯的觀念是,在現代電腦的世界裏,我們可能不止要關心單一事件的效率,同時得考慮該事件的發生頻率,微小的 latency 當頻率很高時就會是一個很可觀的效能瓶頸,反過來說若一個事件發生頻率高,我們若能夠改善該單一事件的效率,整體來說就會有很顯著的效能改善。
目前所看過去學長姐們的作品大多着重在以下幾點改善
優先回顧了由 ryanpatiency 同學所整理的幾個優秀的作品
單次執行
$ perf stat -e cache-misses,cache-references,cycles,instructions ./phonebook_orig
size of entry : 136 bytes
execution time of append() : 0.070619 sec
execution time of findName() : 0.008140 sec
Performance counter stats for './phonebook_orig':
3,530,039 cache-misses # 83.408 % of all cache refs
4,232,257 cache-references
236,836,308 cycles
282,746,456 instructions # 1.19 insn per cycle
0.103359430 seconds time elapsed
執行100次
Performance counter stats for './phonebook_orig' (100 runs):
3,405,962 cache-misses # 89.432 % of all cache refs ( +- 0.06% )
3,808,428 cache-references ( +- 0.18% )
276,828,575 instructions # 1.35 insn per cycle ( +- 0.02% )
204,706,214 cycles ( +- 0.23% )
0.079474756 seconds time elapsed ( +- 0.32% )
在降低 entry size 的優化中主要考量的點應爲 spatial locality,因爲 findName() 的操作中每個 entry 只存取一次,盡可能讓首次寫進 cache 的 entry 越多越能降低 cache miss。
參考大部分同學的做法,將 entry detail 另外存放:
phonebook_opt.h
typedef struct __PHONE_BOOK_ENTRY {
char lastName[MAX_LAST_NAME_SIZE];
struct __PHONE_BOOK_DETAIL *pDetail;
struct __PHONE_BOOK_ENTRY *pNext;
} entry;
typedef struct __PHONE_BOOK_DETAIL {
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;
phonebook_opt.c
entry *append(char lastName[], entry *e)
{
/* TODO: implement */
/* allocate memory for the new entry and put lastName */
e->pNext = (entry *) malloc(sizeof(entry));
e = e->pNext;
strcpy(e->lastName, lastName);
/* Because we won't put any detail data for now, comment it to save memory */
// e->pDetail = (detail *) malloc(sizeof(detail));
e->pNext = NULL;
return e;
}
注意到 pDetail 的實作因爲目前沒有要寫入任何詳細資訊,因此不配置記憶體空間給。
實驗結果
Performance counter stats for './phonebook_opt' (100 runs):
1,207,973 cache-misses # 68.929 % of all cache refs ( +- 0.15% )
1,752,501 cache-references ( +- 0.32% )
255,098,629 instructions # 1.76 insn per cycle ( +- 0.02% )
144,854,000 cycles ( +- 0.40% )
0.056908168 seconds time elapsed ( +- 0.45% )
可以看到 cache-reference 和 cache-miss rate 都有很顯著的下降,整體執行時間加快了約 30%
因爲目前沒有要寫入任何 detail 資料,嘗試將 pDetail 也拿掉比較看看
實驗結果
Performance counter stats for './phonebook_opt' (100 runs):
851,438 cache-misses # 72.021 % of all cache refs ( +- 0.21% )
1,182,206 cache-references ( +- 0.60% )
251,274,431 instructions # 1.81 insn per cycle ( +- 0.02% )
138,847,921 cycles ( +- 0.63% )
0.054831663 seconds time elapsed ( +- 0.65% )
速度稍微快一點,cache-reference 稍微少了一些,但 cache-miss rate 上升了大約 10% 左右。
問題: cache reference 的意義? 爲什麼 cache reference 會下降? Daniel Chen
我們稍微來做一點計算,原始未進行優化的 struct 大小應爲
(MAX_LAST_NAME_SIZE + 16 * 5 + 10 * 2 + 2 + 5)*1 + 8 = 131 (bytes)
但用 gdb 實驗發現實際的大小要比預期得來得大
(gdb) p sizeof(entry)
$2 = 136
調查了一下應該是 Data struct alignment 的問題,因爲我在 64 bits 的系統上執行, align 過後的資料長度恰爲 8 bytes 的倍數。
回到 cache 存取分析,我的 L1 cache 大小是 32 KB, 8-way set associative, cache line size 爲 64 bytes, 136 bytes 需跨越 3 個 cache line, cache hit 只可能發生一次,導致了將近 90% 的 cahce miss。
再來看看優化後的 struct, 大小計算如下
16 + 8 + 8 = 32 (bytes)
恰爲 8 bytes 的倍數,從 gdb 看也是 32 bytes
(gdb) p sizeof(entry)
$1 = 32
一個 cache line 可以存 2 個 entry,若相鄰的 entry 存在連續的記憶體空間中,則每兩次 access 就有可能發生一次 cache hit,結果來看 cache miss 約爲 66%,很接近 1/2。
問題: 以 malloc 配置 struct 是否真的會讓相鄰的 struct 存在於連續的記憶體空間中? Daniel Chen
再進一步看刪掉 pDetail 的結構, entry size 爲 24 bytes,一組 cache line 可存放 2.6 個 entry,平均約 3 次 access 會發生 2 次 cache hit, cache miss 應該約爲 33%,但實際的 cache miss 卻比 32 bytes 的 entry 高出 8%,這樣的計算方法可能有某些問題沒有考慮進去。
爲了取得更多的資訊,我決定額外做一組實驗,設計一大小爲 16 bytes 的 entry,以相同的方法 access,看 cache miss rate 是否能到達預期的 25%。
我將 MAX_LAST_NAME_SIZE 改爲 8,並將 word.txt 中長度大於 8 的字串都截斷 (如: abcdefghijkl 改爲 abcdefgh,此舉可能造成許多重複的內容,但目標字串不會重複所以不影響),實驗結果如下
Performance counter stats for './phonebook_opt' (100 runs):
1,389,173 cache-misses # 76.184 % of all cache refs ( +- 0.13% )
1,823,446 cache-references ( +- 0.35% )
418,142,559 instructions # 2.23 insn per cycle ( +- 0.02% )
187,540,686 cycles ( +- 0.29% )
0.073030731 seconds time elapsed ( +- 0.35% )
效能竟然大幅下降了,顯然我的想法有很大的瑕疵。另一個發現是 IPC 表現較之前的版本好,效能卻下降,此問題也值得探討。
整理目前的問題如下,以下將分別探討這些議題:
Cache reference
此疑問最初來自 perf 原理和實務中的範例程式碼,爲何將 col major 改爲 row major 會造成 cache refs 下降而 cache miss rate 上升(更準確地說是 cache refs & cache miss 都下降,但 cache refs 下降得更多,始得 cache miss rate看起來是升高的),而非原本心中預期的 cache refs 不變且 cache miss count 下降,在此次作業中也有發現做過優化後 cache refs 的次數下降了,但不知道原因爲何。
目前尚未找到合理的解釋,但推測可能有以下問題沒考慮到:
malloc 連續記憶體配置
我以 entry size = 32 bytes 的版本做測試,以 gdb 列印出 pHead -> pNext
臨近的 64 bytes 觀察:
(gdb) x/64xw pHead->pNext
0x5555557588e0: 0x61616161 0x00000000 0x00000000 0x00000000
0x5555557588f0: 0x00000000 0x00000000 0x55758910 0x00005555
0x555555758900: 0x00000000 0x00000000 0x00000031 0x00000000
0x555555758910: 0x61616161 0x00000061 0x00000000 0x00000000
0x555555758920: 0x00000000 0x00000000 0x55758940 0x00005555
0x555555758930: 0x00000000 0x00000000 0x00000031 0x00000000
0x555555758940: 0x61616161 0x00006161 0x00000000 0x00000000
0x555555758950: 0x00000000 0x00000000 0x55758970 0x00005555
0x555555758960: 0x00000000 0x00000000 0x00000031 0x00000000
0x555555758970: 0x61616161 0x00616161 0x00000000 0x00000000
0x555555758980: 0x00000000 0x00000000 0x557589a0 0x00005555
0x555555758990: 0x00000000 0x00000000 0x00000031 0x00000000
0x5555557589a0: 0x61616161 0x61616161 0x00000000 0x00000000
0x5555557589b0: 0x00000000 0x00000000 0x557589d0 0x00005555
0x5555557589c0: 0x00000000 0x00000000 0x00000031 0x00000000
0x5555557589d0: 0x61616161 0x61616161 0x00000068 0x00000000
0x61616161
爲 aaaa
可判斷出這確實是第一筆 entry 的位置(pHead 算第 0 筆),與這組 entry 連結的 pNext
位於 0x00005555 55758910
,中間差距 48 bytes,被 offset 了 16 bytes,繼續往下找的話也可以發現相似的 offset。
實驗 24 bytes 的版本:
(gdb) x/64x pHead->pNext
0x5555557588d0: 0x61616161 0x00000000 0x00000000 0x00000000
0x5555557588e0: 0x557588f0 0x00005555 0x00000021 0x00000000
0x5555557588f0: 0x61616161 0x00000061 0x00000000 0x00000000
0x555555758900: 0x55758910 0x00005555 0x00000021 0x00000000
0x555555758910: 0x61616161 0x00006161 0x00000000 0x00000000
0x555555758920: 0x55758930 0x00005555 0x00000021 0x00000000
0x555555758930: 0x61616161 0x00616161 0x00000000 0x00000000
0x555555758940: 0x55758950 0x00005555 0x00000021 0x00000000
0x555555758950: 0x61616161 0x61616161 0x00000000 0x00000000
0x555555758960: 0x55758970 0x00005555 0x00000021 0x00000000
0x555555758970: 0x61616161 0x61616161 0x00000068 0x00000000
0x555555758980: 0x55758990 0x00005555 0x00000021 0x00000000
0x555555758990: 0x61616161 0x75616161 0x00686775 0x00000000
0x5555557589a0: 0x557589b0 0x00005555 0x00000021 0x00000000
0x5555557589b0: 0x61616161 0x68676161 0x00000000 0x00000000
0x5555557589c0: 0x557589d0 0x00005555 0x00000021 0x00000000
offset 變成 8 bytes,兩個 entry 距離為 32 bytes。
找不到共通點,此問題可留待後續研究,但可以確定的事是,連續 malloc 並不保證記憶體空間連續,此問題可用 memory pool 解決,一開始就先配置一組連續的記憶體,如此也能使有用的資料寫進 cache 的機會增加。另一解決方法爲使用 array 來 maintain 一組 hash table,但 hash 就不保證在存取時會按照順序存取了。
後來發現 malloc 是以 "Chunk" 為單位進行分配, chunk 的長度是可變的,但因 alignment 的問題一定是 8 bytes 的倍數,當程式進行記憶體配置要求的時候, malloc 會在可用的 chunk 中拼湊出一塊連續、空間足夠的 chunk,而每個 chunck 前面會有該 chunk 的大小資訊,特別的地方是因為 chunck 的大小一定是 8 bytes 的倍數,這個大小資訊的 3 個 LSB 會一直是 0 因此這三個 bits 被拿來用作 flag,分別代表
觀察上面的例子,在 32 bytes 的 case 中, size 資訊為 0x31
清除最後 3 個 bits 後是 0x30 = 48 (dec)
恰為兩個 entry 間的距離。而 24 bytes 的 case 中 size 為 0x21
一樣可以看出 chunck 大小為 0x20 = 32 (dec)
,並且可驗證 prev_in_use bit 為 1,表示前一個 chunck 使用中。
[ref]
縮減 entry size 帶來的效益
原本預期 entry size 越小應可降低 cache miss 發生的機會,但結果並不盡然。
此問題初步認為應和 (1) 的 cache refs 行為有關系,因為尚不瞭解 (1) 的詳細原理,此問題暫時擱置。 team6612
使用 Mem pool 的好處是可配置一塊連續的記憶體空間給 linked list,讓寫進 cache 內的 entry 增加,降低 cache miss,此外也可以節省多次 malloc 所產生出來額外的 chuck info 造成記憶體空間的浪費,但必須注意 pool 的空間管理,避免存取到非 pool 範圍的記憶體,以及使用完後需將空間釋放,否則將造成 memory leak。
Memeory pool 實作如下
phonebook_opt.h
typedef struct _mpool {
char *head;
char *next;
char *tail;
} mpool;
mpool *pool_create(size_t s);
void *pool_alloc(mpool *p, size_t size);
void pool_free(mpool *p);
phonebook_opt.c
mpool *pool_create(size_t size)
{
mpool *p = (mpool *) malloc(sizeof(mpool));
p->head = p->next = (char *) malloc(size);
p->tail = p->head + size;
return p;
}
void *pool_alloc(mpool *p, size_t size)
{
if (p->tail - p->next < size)
return NULL;
void *m = (void *) p->next;
p->next += size;
return m;
}
void pool_free(mpool *p)
{
free(p->head);
free(p);
}
修改 main.c
...
#ifdef OPT
mpool *pool = pool_create(sizeof(entry) * 400000);
#endif
...
#ifdef OPT
pHead = (entry *) pool_alloc(pool, sizeof(entry));
#else
pHead = (entry *) malloc(sizeof(entry));
#endif
...
#ifdef OPT
e = append(line, e, pool);
#else
e = append(line, e);
#endif
...
#ifdef OPT
/* free mem pool */
pool_free(pool);
#else
/* free linked list of entry */
do {
e = pHead;
pHead = pHead->pNext;
free(e);
} while (pHead);
#endif
實驗結果
Performance counter stats for './phonebook_opt' (100 runs):
637,029 cache-misses # 74.870 % of all cache refs ( +- 0.10% )
850,844 cache-references ( +- 0.39% )
181,992,282 instructions # 1.81 insn per cycle ( +- 0.03% )
100,383,644 cycles ( +- 0.22% )
0.039542202 seconds time elapsed ( +- 0.32% )
可觀察到整體執行時間下降, cache-misses 總數也下降了,其中 append()
的執行時間較 findName()
明顯,應是因爲省去了反覆呼叫 malloc 所造成的一些額外的 overhead。
以 valgrind 檢查一下是否有 memory leak (編譯時記得加上 -g
)
$ valgrind --leak-check=full ./phonebook_opt
...
==7363== HEAP SUMMARY:
==7363== in use at exit: 0 bytes in 0 blocks
==7363== total heap usage: 7 allocs, 7 frees, 9,610,344 bytes allocated
==7363==
==7363== All heap blocks were freed -- no leaks are possible
以 gdb 觀察記憶體配置狀況
(gdb) x/64x pHead->pNext
0x7ffff70cd028: 0x61616161 0x00000000 0x00000000 0x00000000
0x7ffff70cd038: 0xf70cd040 0x00007fff 0x61616161 0x00000061
0x7ffff70cd048: 0x00000000 0x00000000 0xf70cd058 0x00007fff
0x7ffff70cd058: 0x61616161 0x00006161 0x00000000 0x00000000
0x7ffff70cd068: 0xf70cd070 0x00007fff 0x61616161 0x00616161
0x7ffff70cd078: 0x00000000 0x00000000 0xf70cd088 0x00007fff
0x7ffff70cd088: 0x61616161 0x61616161 0x00000000 0x00000000
0x7ffff70cd098: 0xf70cd0a0 0x00007fff 0x61616161 0x61616161
0x7ffff70cd0a8: 0x00000068 0x00000000 0xf70cd0b8 0x00007fff
0x7ffff70cd0b8: 0x61616161 0x75616161 0x00686775 0x00000000
0x7ffff70cd0c8: 0xf70cd0d0 0x00007fff 0x61616161 0x68676161
0x7ffff70cd0d8: 0x00000000 0x00000000 0xf70cd0e8 0x00007fff
0x7ffff70cd0e8: 0x61616161 0x68686161 0x00686868 0x00000000
0x7ffff70cd0f8: 0xf70cd100 0x00007fff 0x61616161 0x67756161
0x7ffff70cd108: 0x00000068 0x00000000 0xf70cd118 0x00007fff
0x7ffff70cd118: 0x61616161 0x00686761 0x00000000 0x00000000
可發現 entry 間已經沒有多餘的資訊了。
問題討論
效能改進策略
本例選用的 dataset (也就是輸入電話簿的姓名) 是否存在問題?
strncpy
避免陣列溢出