天天看點

多核程式設計:選擇合适的結構體大小,提高多核并發性能

作者:[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.     cpu_set_t cpu_set;
  3.     int i;
  4.     ret = pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpu_set);
  5.         printf("check err!\n");
  6.         return;
  7.     for (i = 0; i < CPU_SETSIZE; ++i) {
  8.         if (CPU_ISSET(i, &cpu_set)) {
  9.             printf("cpu %d\n", i);
  10.         }
  11. #define CPU_NR          2
  12. #define CACHE_LINE_SIZE 64
  13. #define VAR_NR ((CACHE_LINE_SIZE/sizeof(int))-1)
  14. //這個結構為多核程式設計中最頻繁使用的結構
  15. //其size大小為本文重點
  16. struct key {
  17.     int a[VAR_NR];
  18.     //int pad;
  19. } __attribute__((packed));
  20. //使用空間換時間,每個CPU擁有不同的資料
  21. static struct key g_key[CPU_NR];

  //醜陋的寫死——這裡僅僅為了說明問題,我就不改了。

  1. static void real_job(int index)
  2. #define LOOP_NR 100000000
  3.     struct key *k = g_key+index;
  4.     for (i = 0; i < VAR_NR; ++i) {
  5.         k->a[i] = i;
  6.     for (i = 0; i < LOOP_NR; ++i) {
  7.         k->a[14] = k->a[14]+k->a[3];
  8.         k->a[3] = k->a[14]+k->a[5];
  9.         k->a[1] = k->a[1]+k->a[7];
  10.         k->a[7] = k->a[1]+k->a[9];
  11. static volatile int thread_ready = 0;

  //這裡使用醜陋的寫死。最好是通過參數來設定親和的CPU

  //這個線程運作在CPU 1上

  1. static void *thread_task(void *data)
  2.     set_thread_affinity(1);
  3.     check_cpu_affinity();
  4.     thread_ready = 1;
  5.     real_job(1);
  6.     return NULL;
  7. int main(int argc, char *argv[])
  8.     pthread_t tid;

     //設定主線程運作在CPU 0上

  1.     ret = set_thread_affinity(0);
  2.         printf("err1\n");

     //提高優先級,避免程序被換出。因為換出後,cache會失效,會影響測試效果

  1.     ret = nice(-20);
  2.     if (-1 == ret) {
  3.         printf("err2\n");
  4.     ret = pthread_create(&tid, NULL, thread_task, NULL);

     //忙等待,使兩個real_job同時進行

  1.     while (!thread_ready)
  2.         ;
  3.     real_job(0);
  4.     pthread_join(tid, NULL);
  5.     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,進而影響性能。

下面編譯運作:

  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. real 0m10.081s
  10. user 0m20.074s
  11. sys 0m0.010s
  12. real 0m9.989s
  13. 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已經獨立,不會互相影響了。

請看測試結果:

  1. [root@Lnx99 cache]#gcc -g -Wall cache_line.c -lpthread -o padd
  2. [root@Lnx99 cache]#time ./padd
  3. real 0m1.824s
  4. user 0m3.614s
  5. sys 0m0.012s
  6. real 0m1.817s
  7. user 0m3.625s
  8. user 0m3.613s

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

從這個例子中,我們應該學到

1. CPU的cache對于提高程式性能非常重要!一個良好的設計,可以保證更高的cache hit,進而得到更好的性能;

2. 多核程式設計中,對于cache line一定要格外關注。關鍵結構體size大小的控制和選擇,可以大幅提高多核的性能;

3. 在多核程式設計中,寫程式時,一定要思考,思考,再思考