contributed by <jayfeng0225
>
jayfeng0225
phonebook_opt.c
和相關的檔案alloc()
的時間成本
先對Concurrency與Parallelism有最基本的了解:
oncurrency Concurrency + parallelism
___ ___ ___
|th1| |th1|th2|
| | | |___|
|___|___ | |___
|th2| |___|th2|
___|___| ___|___|
|th1| |th1|
|___|___ | |___
|th2| | |th2|
Speedups in any of these areas will directly lead to speedups in sequential (nonparallel, single-threaded, single-process) applications, as well as applications that do make use of concurrency. That’s important, because the vast majority of today’s applications are single-threaded, for good reasons that I’ll get into further below.
儘管CPU這幾年來在clock speed上並沒有卓越的成長(近幾年已經有4Ghz的CPU出現),但CPU的效能還是有所提升,其中仰賴的是多處理器技術時代的三種主要方法:
平行執行兩個以上的多執行緒在單個CPU上。已經是目前(2005)很普遍的做法,不過hyperthread的做法需要仰賴額外的硬體像是額外的暫存器,而其他如cache , integer math unit , FPU(floating point unit)等等也都只有單個。
而Hyperthrading的方法即使在寫得好的多執行緒程式上,最多也能夠提升5%~15%的效能,甚至在最理想的環境下最多也就只能提升到40%。雖然很好,但效能幾乎無法double,也因此無法幫助單執行緒的應用程式。
多核心表示在晶片上執行兩個以上的實體CPU。效能初始大約可以相近於實體的雙核心(CPU)系統,這代表某些情況可能不會到兩倍的速度,而multicore只能夠提升寫得好的多執行緒程式,而無法提升單執行緒的應用程式。
最後提到的是cache,on-die cache的size持續在成長。提到的三個方法中只有這個方法能夠大大的讓多數既有的應用程式受惠。最簡單的原因就是,以空間換取速度。通常存取Main memory的速度會遠大於存取cache的速度(10~50倍),因此如果執行應用程式的working set能夠符合cache的大小,那麼效率就會很好。這也就是為什麼增加cache size能夠讓多數的程式受惠,不論是單執行緒或是多執行緒的程式。隨著程式所需要的data越來越多,會需要更頻繁的update,也因此如果能夠符合cache,執行效能上就能夠大大提升。
on-die cache : 是指內建在晶片上的快取記憶體,目前通常是指L1,L2 cache。
相對於cache能夠提升程式效能,那麼hyperthreading與multicore對於程式幾乎沒什麼影響。因此,在硬體上做的這些改變表示我們要怎麼去設計我們的軟體。 –> concurrency
Concurrency,特別是multithreading已經作為主流的軟體設計有兩個主要理由 :
Concurrency的cost :
locks的取得成本較高,但如果使用的好,那麼可以從concurrent code的到的好處會遠大於在synchronization的所失去的成本。
第二個concurrency的成本為,不是所有的application都是適合平行化的。
第三個最大的成本為,要實踐concurrency是很困難的。
Martin Fowler的定義是「在不改變軟體外部行為的前提下,改變其內部結構,使其更容易理解且易於修改」
程式碼的maintainability? JayFeng
從這個定義上來看,為軟體增加功能肯定不能算上重構。那麼改善安全性或者是提升效能,算不算是重構呢?顯然也不能。
改寫程式碼,使得程式碼在不改變行為的情況執行得更快,這應該算最佳化而不能算是重構。
不過,也有一些人把這類型的「整理」,歸類在重構的範圍,這可以說是與Martin Fowler的原意不甚相符了。
所以,我們不能說提升效能是重構的目的,但是透過重構,使得程式碼更清晰易懂,那麼對於察覺既有的各種問題,都可以提供相當的幫助
目前找到phonebook_opt.c的for loop似乎可以改用while loop來表示,原先的版本在for的更新值部份似乎改用while看起來會比較順眼。
修改的版本:
char *i = app->ptr;
while(i < app->eptr)
{
app->pLast->pNext = j;
app->pLast = app->pLast->pNext;
app->pLast->lastName = i;
dprintf("thread %d append string = %s\n",
app->tid, app->pLast->lastName);
app->pLast->pNext = NULL;
// 更新值
i += MAX_LAST_NAME_SIZE * app->nthread;
j += app->nthread;
count++;
//
//
}
Reference : 記憶體映射函數 mmap 的使用方法
linux提供記憶體映射函數,可以把文件內容直接映射到一段VMA。透過對這段記憶體的讀取與修改,實現對文件的讀取修改。
使用mmap有以下好處:
使用方法 :
函數:void *mmap(void *start,size_t length, int prot, int flags, int fd, off_t offsize);
參數start:指向欲映射的核心起始位址,通常設為NULL,代表讓系統自動選定位址,核心會自己在進程位址空間中選擇合適的位址建立映射。映射成功後返回該位址。如果不是NULL,則給核心一個提示,應該從什麼位址開始映射,核心會選擇start之上的某個合適的位址開始映射。建立映射後,真正的映射位址通過返回值可以得到。
參數length:代表映射的大小。將文件的多大長度映射到記憶體。
參數prot:映射區域的保護方式。可以為以下幾種方式的組合:
PROT_EXEC 映射區域可被執行
PROT_READ 映射區域可被讀取
PROT_WRITE 映射區域可被寫入
PROT_NONE 映射區域不能存取
MAP_FIXED 如果參數start所指的位址無法成功建立映射時,則放棄映射,不對位址做修正。通常不鼓勵用此旗標。
MAP_SHARED 允許其他映射該文件的行程共享,對映射區域的寫入數據會複製回文件。
MAP_PRIVATE 不允許其他映射該文件的行程共享,對映射區域的寫入操作會產生一個映射的複製(copy-on-write),對此區域所做的修改不會寫回原文件。
MAP_ANONYMOUS 建立匿名映射。此時會忽略參數fd,不涉及文件,而且映射區域無法和其他進程共享。
MAP_DENYWRITE 只允許對映射區域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。
參數fd:由open返回的文件描述符,代表要映射到核心中的文件。如果使用匿名核心映射時,即flags中設置了MAP_ANONYMOUS,fd設為-1。有些系統不支持匿名核心映射,則可以使用fopen打開/dev/zero文件,然後對該文件進行映射,可以同樣達到匿名核心映射的效果。
參數offset:從文件映射開始處的偏移量,通常為0,代表從文件最前方開始映射。offset必須是分頁大小的整數倍(在32位體系統結構上通常是4K)。
範例程式:小談 mmap() 與 VMA
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#define FILE_LENGTH 0x400
int main(int argc, char *argv[])
{
int fd;
void *map_memory;
/* Open a file to be mapped. */
fd = open("/tmp/shared_file", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
lseek(fd, FILE_LENGTH+1, SEEK_SET);
write(fd, "", 1);
lseek(fd, 0, SEEK_SET);
/* Create map memory. */
map_memory = mmap(0, FILE_LENGTH, PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
/* Write to mapped memory. */
if (strlen(argv[1]) < FILE_LENGTH)
sprintf((char *)map_memory, "%s", argv[1]);
sleep(10);
exit(0);
}
Reference :
Getting Started With POSIX Threads 宋振華
How to Create Threads in Linux (With a C Example Program)
mmap
char *map = mmap(NULL, fs, PROT_READ, MAP_SHARED, fd, 0);
原始版本優化新增thread的部份
/* allocate at beginning */
entry *entry_pool = (entry *) malloc(sizeof(entry) *
fs / MAX_LAST_NAME_SIZE);
assert(entry_pool && "entry_pool error");
pthread_setconcurrency(THREAD_NUM + 1);
//用來通知實作concurrency的函數
//宣告pthread與用來實作thread append的struct
pthread_t *tid = (pthread_t *) malloc(sizeof(pthread_t) * THREAD_NUM);
append_a **app = (append_a **) malloc(sizeof(append_a *) * THREAD_NUM);
//new_append_a(char *ptr,char *eptr, int tid , int ntd);
for (int i = 0; i < THREAD_NUM; i++)
app[i] = new_append_a(map + MAX_LAST_NAME_SIZE * i, map + fs, i,
THREAD_NUM, entry_pool + i);
clock_gettime(CLOCK_REALTIME, &mid);
for (int i = 0; i < THREAD_NUM; i++)
pthread_create( &tid[i], NULL, (void *) &append, (void *) app[i]);
for (int i = 0; i < THREAD_NUM; i++)
pthread_join(tid[i], NULL);
使用entry pool並且建立4個thread,設立類似分界點的概念。每個thread各自負責一個分界裡的append,最後再各自串起來。
另外則是使用pthread_join()函數,這樣的缺點可能會變成:必須等待前一個thread完成,下一個thread才會開始執行,因此當thread數量變多時,相對的等待時間也許會更長。假如使用Thread pool,更加彈性的使用每個thread,也許就可以減少append的時間。
基於這個假設,先畫出不同thread數量下append所需要的時間:
$ man pthread_join
NAME
pthread_join -- wait for thread termination
ESCRIPTION
The pthread_join() function suspends execution of the calling thread until the target
thread terminates, unless the target thread has already terminated.
假如不使用pthread_join() ?
pthread simple example: pthread_join
pthread_join 是等待其他的執行緒結束後才會return
寫的代碼中如果沒有pthread_join主執行緒會很快結束從而使整個process結束,從而使創建的兩個執行緒沒有機會開始執行就結束了,所以沒有輸出。加入pthread_join後,主thread會一直等待直到等待的執行緒結束自己才結束,使創建的執行緒有機會執行。
pthread_create 一建立執行緒之後即開始執行
如果對於創建的執行緒如果不調用pthread_join,會造成什麼樣的後果
有可能子執行緒沒被執行完就退出主線程了!
能不能說清楚一下點,為什麼會這樣,如果在執行緒裡加上控制,應該就不會出現這個問題吧
When a joinable thread terminates, its memory resources (thread
descriptor and stack) are not deallocated until another thread performs
pthread_join on it. Therefore, pthread_join must be called once for
each joinable thread created to avoid memory leaks.
程式的主執行緒結束了, 這個執行緒的process就over了。 process結束時,其他執行緒就被清理了。也就是執行不到了。主要就是防止在子執行緒結束完之前結束主執行緒。
所以說,加個pthread_join,既可防止內存洩露,又可以知道子執行緒的退出狀態,而不是所謂的子執行緒無法運行下去。要知道執行緒還有一個狀態:DETACHED,就不需要pthread_join
pthread_join和pthread_detach的區別
但是調用pthread_join(pthread_id)後,如果該執行緒沒有運行結束,調用者會被阻塞
Reference :
所以試著將程式改為thread pool的方式 :