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

多核编程:选择合适的结构体大小,…

2013-12-19 20:48 176 查看
作者: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 <</span>pthread.h>

#include <</span>sched.h>

#include <</span>stdio.h>

#include <</span>stdlib.h>

#include <</span>errno.h>

#include <</span>sys/types.h>

#include <</span>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 <</span> 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 <</span> VAR_NR; ++i) {

k->a[i] = i;

}

for (i = 0; i <</span> 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.
在多核编程中,写程序时,一定要思考,思考,再思考
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: