多核編程:選擇合適的結構體大小,提高多核併發性能

作者:[email protected]
博客: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)

  1. #define _GNU_SOURCE
  2. #include <pthread.h>
  3. #include <sched.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <errno.h>
  7. #include <sys/types.h>
  8. #include <unistd.h>

  9. // 設置線程的CPU親和性,使不同線程同時運行在不同的CPU上
  10. static int set_thread_affinity(int cpu_id)
  11. {
  12.     cpu_set_t cpuset;
  13.     int ret;

  14.     CPU_ZERO(&cpuset);
  15.     CPU_SET(cpu_id, &cpuset);

  16.     ret = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
  17.     if (ret != 0) {
  18.         printf("set affinity error\n");
  19.         return -1;
  20.     }

  21.     return 0;
  22. }

 //檢查線程的CPU親和性
  1. static void check_cpu_affinity(void)
  2. {
  3.     cpu_set_t cpu_set;
  4.     int ret;
  5.     int i;

  6.     ret = pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpu_set);
  7.     if (ret != 0) {
  8.         printf("check err!\n");
  9.         return;
  10.     }

  11.     for (= 0; i < CPU_SETSIZE; ++i) {
  12.         if (CPU_ISSET(i, &cpu_set)) {
  13.             printf("cpu %d\n", i);
  14.         }
  15.     }

  16. }


  17. #define CPU_NR          2
  18. #define CACHE_LINE_SIZE 64
  19. #define VAR_NR ((CACHE_LINE_SIZE/sizeof(int))-1)
  20. //這個結構爲多核編程中最頻繁使用的結構
  21. //其size大小爲本文重點
  22. struct key {
  23.     int a[VAR_NR];
  24.     //int pad;
  25. } __attribute__((packed));
  26. //使用空間換時間,每個CPU擁有不同的數據
  27. static struct key g_key[CPU_NR];

  //醜陋的硬編碼——這裏僅僅爲了說明問題,我就不改了。
  1. static void real_job(int index)
  2. {
  3. #define LOOP_NR 100000000
  4.     struct key *= g_key+index;

  5.     int i;
  6.     for (= 0; i < VAR_NR; ++i) {
  7.         k->a[i] = i;
  8.     }

  9.     for (= 0; i < LOOP_NR; ++i) {
  10.         k->a[14] = k->a[14]+k->a[3];
  11.         k->a[3] = k->a[14]+k->a[5];
  12.         k->a[1] = k->a[1]+k->a[7];
  13.         k->a[7] = k->a[1]+k->a[9];
  14.     }
  15. }

  16. static volatile int thread_ready = 0;

  //這裏使用醜陋的硬編碼。最好是通過參數來設置親和的CPU
  //這個線程運行在CPU 1上
  1. static void *thread_task(void *data)
  2. {
  3.     set_thread_affinity(1);
  4.     check_cpu_affinity();

  5.     thread_ready = 1;

  6.     real_job(1);

  7.     return NULL;
  8. }

  9. int main(int argc, char *argv[])
  10. {
  11.     pthread_t tid;
  12.     int ret;
 
     //設置主線程運行在CPU 0上
  1.     ret = set_thread_affinity(0);
  2.     if (ret != 0) {
  3.         printf("err1\n");
  4.         return -1;
  5.     }
  6.     check_cpu_affinity();

     //提高優先級,避免進程被換出。因爲換出後,cache會失效,會影響測試效果
  1.     ret = nice(-20);
  2.     if (-== ret) {
  3.         printf("err2\n");
  4.         return -1;
  5.     }

  6.     ret = pthread_create(&tid, NULL, thread_task, NULL);
  7.     if (ret != 0) {
  8.         printf("err2\n");
  9.         return -1;
  10.     }

     //忙等待,使兩個real_job同時進行
  1.     while (!thread_ready)
  2.         ;

  3.     real_job(0);

  4.     pthread_join(tid, NULL);

  5.     printf("Completed!\n");

  6.     return 0;
  7. }

感興趣的同學,可以修改這代碼,使其運行更多的線程來測試。但是一定注意你的平臺的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,從而影響性能。

下面編譯運行:
  1. [root@Lnx99 cache]#gcc -g -Wall cache_line.c -lpthread -o no_padd
  2. [root@Lnx99 cache]#time ./no_padd
  3. cpu 0
  4. cpu 1
  5. Completed!
  6. real 0m9.830s
  7. user 0m19.427s
  8. sys 0m0.011s
  9. [root@Lnx99 cache]#time ./no_padd
  10. cpu 0
  11. cpu 1
  12. Completed!
  13. real 0m10.081s
  14. user 0m20.074s
  15. sys 0m0.010s
  16. [root@Lnx99 cache]#time ./no_padd
  17. cpu 0
  18. cpu 1
  19. Completed!
  20. real 0m9.989s
  21. user 0m19.877s
  22. sys 0m0.010s
下面我們把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已經獨立,不會互相影響了。

請看測試結果:
  1. [root@Lnx99 cache]#gcc -g -Wall cache_line.c -lpthread -o padd
  2. [root@Lnx99 cache]#time ./padd
  3. cpu 0
  4. cpu 1
  5. Completed!
  6. real 0m1.824s
  7. user 0m3.614s
  8. sys 0m0.012s
  9. [root@Lnx99 cache]#time ./padd
  10. cpu 0
  11. cpu 1
  12. Completed!
  13. real 0m1.817s
  14. user 0m3.625s
  15. sys 0m0.011s
  16. [root@Lnx99 cache]#time ./padd
  17. cpu 0
  18. cpu 1
  19. Completed!
  20. real 0m1.824s
  21. user 0m3.613s
  22. sys 0m0.011s

結果有些出人意料吧。同樣的代碼,僅僅是更改了關鍵結構體的大小,性能卻相差了近10倍!

從這個例子中,我們應該學到
1. CPU的cache對於提高程序性能非常重要!一個良好的設計,可以保證更高的cache hit,從而得到更好的性能;
2. 多核編程中,對於cache line一定要格外關注。關鍵結構體size大小的控制和選擇,可以大幅提高多核的性能;
3. 在多核編程中,寫程序時,一定要思考,思考,再思考

發佈了15 篇原創文章 · 獲贊 9 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章