部落格:blog.focus-linux.net linuxfocus.blog.chinaunix.net
本文的copyleft歸[email protected]所有,使用GPL釋出,可以自由拷貝,轉載。但轉載請保持文檔的完整性,注明原作者及原連結,嚴禁用于任何商業用途。
======================================================================================================
在現代的程式設計中,多核程式設計已經是很普遍的應用了。多核程式設計究竟有什麼不同?我們如何提高多核程式設計的性能?針對這個問題,我們需要了解多核與單核在體系架構上有什麼不同。
由于本文不是用于介紹多核架構的文章,是以不準備對其架構進行展開。感興趣的朋友可以自行搜尋google。今天就說其中的一點。大家都知道現代的CPU都具有cache,用于提高CPU通路指令或者資料的速度——一般來說,指令cache和資料cache是分開的,因為這樣性能更好。在cache的比對和通路過程中,cache的最小單元是line,即cache line,有的也稱其為cache的data block。之是以稱為block,因為在cache中存的不是記憶體傳遞的最小單元(字),而是多個字——32位機,一個字為4個bytes。當cache miss的時候,CPU從記憶體中預取一個data block大小的資料,放到cache中。(這裡隻是一個極其簡單的描述,準确具體請google)。
回歸正題。在多核程式設計下,cache line又是如何影響多核的性能的呢。比如有兩個CPU,CPU1要修改一個變量var的值。這時var是在CPU1的cache中的,var的值被更新。那麼萬一CPU2的cache中也有var怎麼辦?為了保證資料的一緻性,CPU1需要使CPU2中var變量對應的cache line失效或者将其同樣更新為最新值。一般來說,使其失效更為普遍。如果使失效,那麼當CPU2要通路var時,會産生一次cache miss。如果使其更新,同樣要涉及更新CPU2的cache line操作,都是要損失一定性能的。
在多核程式設計的時候,為了保證并發性,往往使用空間來換取時間,讓每個CPU通路獨立的變量或者per cpu的變量,來避免加鎖。這是一種很常見的多核程式設計技巧。一般的簡單實作,都是使用數組來實作,其中數組的個數為CPU的個數。那麼,在這個時候,該變量就需要選用一個适當的size,來避免多核cache失效帶來的性能下降。
下面看執行個體。(我的硬體平台:雙核Intel(R) Pentium(R) 4 CPU,這個CPU的cache line為64 bytes)
- #define _GNU_SOURCE
- #include <pthread.h>
- #include <sched.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <errno.h>
- #include <sys/types.h>
- #include <unistd.h>
- // 設定線程的CPU親和性,使不同線程同時運作在不同的CPU上
- static int set_thread_affinity(int cpu_id)
- {
- cpu_set_t cpuset;
- int ret;
- CPU_ZERO(&cpuset);
- CPU_SET(cpu_id, &cpuset);
- ret = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
- if (ret != 0) {
- printf("set affinity error\n");
- return -1;
- }
- return 0;
- }
//檢查線程的CPU親和性
- static void check_cpu_affinity(void)
- cpu_set_t cpu_set;
- int i;
- ret = pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpu_set);
- printf("check err!\n");
- return;
- for (i = 0; i < CPU_SETSIZE; ++i) {
- if (CPU_ISSET(i, &cpu_set)) {
- printf("cpu %d\n", i);
- }
- #define CPU_NR 2
- #define CACHE_LINE_SIZE 64
- #define VAR_NR ((CACHE_LINE_SIZE/sizeof(int))-1)
- //這個結構為多核程式設計中最頻繁使用的結構
- //其size大小為本文重點
- struct key {
- int a[VAR_NR];
- //int pad;
- } __attribute__((packed));
- //使用空間換時間,每個CPU擁有不同的資料
- static struct key g_key[CPU_NR];
//醜陋的寫死——這裡僅僅為了說明問題,我就不改了。
- static void real_job(int index)
- #define LOOP_NR 100000000
- struct key *k = g_key+index;
- for (i = 0; i < VAR_NR; ++i) {
- k->a[i] = i;
- for (i = 0; i < LOOP_NR; ++i) {
- k->a[14] = k->a[14]+k->a[3];
- k->a[3] = k->a[14]+k->a[5];
- k->a[1] = k->a[1]+k->a[7];
- k->a[7] = k->a[1]+k->a[9];
- static volatile int thread_ready = 0;
//這裡使用醜陋的寫死。最好是通過參數來設定親和的CPU
//這個線程運作在CPU 1上
- static void *thread_task(void *data)
- set_thread_affinity(1);
- check_cpu_affinity();
- thread_ready = 1;
- real_job(1);
- return NULL;
- int main(int argc, char *argv[])
- pthread_t tid;
//設定主線程運作在CPU 0上
- ret = set_thread_affinity(0);
- printf("err1\n");
//提高優先級,避免程序被換出。因為換出後,cache會失效,會影響測試效果
- ret = nice(-20);
- if (-1 == ret) {
- printf("err2\n");
- ret = pthread_create(&tid, NULL, thread_task, NULL);
//忙等待,使兩個real_job同時進行
- while (!thread_ready)
- ;
- real_job(0);
- pthread_join(tid, NULL);
- printf("Completed!\n");
感興趣的同學,可以修改這代碼,使其運作更多的線程來測試。但是一定注意你的平台的cache line的大小。
第一次,關鍵結構struct key的size為60位元組。這樣主線程CPU 0 在通路g_key[0]的時候,其對應的cache line包含了g_key[1]的開頭部分的資料。那麼當主線程更新g_key[0]的值時,會使CPU 1的cache失效,導緻CPU1 通路g_key[1]的部分資料時産生cache miss,進而影響性能。
下面編譯運作:
- [root@Lnx99 cache]#gcc -g -Wall cache_line.c -lpthread -o no_padd
- [root@Lnx99 cache]#time ./no_padd
- cpu 0
- cpu 1
- Completed!
- real 0m9.830s
- user 0m19.427s
- sys 0m0.011s
- real 0m10.081s
- user 0m20.074s
- sys 0m0.010s
- real 0m9.989s
- user 0m19.877s
下面我們把int pad前面的//去掉,使struct key的size變為64位元組,即與cache line比對。這時CPU 0修改g_key[0]時就不會影響CPU 1的cache。因為g_key[1]的資料不包含在g_key[0]所在的CPU 0的cache中。也就是說g_key[0]和g_key[1]的所在的cache line已經獨立,不會互相影響了。
請看測試結果:
- [root@Lnx99 cache]#gcc -g -Wall cache_line.c -lpthread -o padd
- [root@Lnx99 cache]#time ./padd
- real 0m1.824s
- user 0m3.614s
- sys 0m0.012s
- real 0m1.817s
- user 0m3.625s
- user 0m3.613s
結果有些出人意料吧。同樣的代碼,僅僅是更改了關鍵結構體的大小,性能卻相差了近10倍!
從這個例子中,我們應該學到
1. CPU的cache對于提高程式性能非常重要!一個良好的設計,可以保證更高的cache hit,進而得到更好的性能;
2. 多核程式設計中,對于cache line一定要格外關注。關鍵結構體size大小的控制和選擇,可以大幅提高多核的性能;
3. 在多核程式設計中,寫程式時,一定要思考,思考,再思考