作者:[email protected]
博客:blog.focus-linux.net linuxfocus.blog.chinaunix.net
博客: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 ret;
- int i;
- ret = pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpu_set);
- if (ret != 0) {
- 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;
- int i;
- 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;
- int ret;
//设置主线程运行在CPU 0上
- ret = set_thread_affinity(0);
- if (ret != 0) {
- printf("err1\n");
- return -1;
- }
- check_cpu_affinity();
//提高优先级,避免进程被换出。因为换出后,cache会失效,会影响测试效果
- ret = nice(-20);
- if (-1 == ret) {
- printf("err2\n");
- return -1;
- }
- ret = pthread_create(&tid, NULL, thread_task, NULL);
- if (ret != 0) {
- printf("err2\n");
- return -1;
- }
//忙等待,使两个real_job同时进行
- while (!thread_ready)
- ;
- real_job(0);
- pthread_join(tid, NULL);
- printf("Completed!\n");
- return 0;
- }
感兴趣的同学,可以修改这代码,使其运行更多的线程来测试。但是一定注意你的平台的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
- [root@Lnx99 cache]#time ./no_padd
- cpu 0
- cpu 1
- Completed!
- real 0m10.081s
- user 0m20.074s
- sys 0m0.010s
- [root@Lnx99 cache]#time ./no_padd
- cpu 0
- cpu 1
- Completed!
- real 0m9.989s
- user 0m19.877s
- sys 0m0.010s
请看测试结果:
- [root@Lnx99 cache]#gcc -g -Wall cache_line.c -lpthread -o padd
- [root@Lnx99 cache]#time ./padd
- cpu 0
- cpu 1
- Completed!
- real 0m1.824s
- user 0m3.614s
- sys 0m0.012s
- [root@Lnx99 cache]#time ./padd
- cpu 0
- cpu 1
- Completed!
- real 0m1.817s
- user 0m3.625s
- sys 0m0.011s
- [root@Lnx99 cache]#time ./padd
- cpu 0
- cpu 1
- Completed!
- real 0m1.824s
- user 0m3.613s
- sys 0m0.011s
结果有些出人意料吧。同样的代码,仅仅是更改了关键结构体的大小,性能却相差了近10倍!
从这个例子中,我们应该学到
1. CPU的cache对于提高程序性能非常重要!一个良好的设计,可以保证更高的cache hit,从而得到更好的性能;
2. 多核编程中,对于cache line一定要格外关注。关键结构体size大小的控制和选择,可以大幅提高多核的性能;
3. 在多核编程中,写程序时,一定要思考,思考,再思考