您的位置:首页 > 编程语言

多核编程:选择合适的结构体大小,提高多核并发性能

2014-03-30 10:36 246 查看
作者:gfree.wind@gmail.com

博客:blog.focus-linux.net linuxfocus.blog.chinaunix.net

本文的copyleft归gfree.wind@gmail.com所有,使用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

下面我们把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
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. 在多核编程中,写程序时,一定要思考,思考,再思考

原文链接:http://blog.chinaunix.net/uid-23629988-id-3212520.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: