在高性能、高并发程序编写过程中,如果合理利用cpu本身特性,能够大幅提供程序的性能,降低cpu占用。本文将介绍利用cache、numa特性,降低cpu占用的手段
在linux命令行中,执行lscpu,可以看到cpu的详细信息,如下所示:
Architecture: x86_64 //系统架构x86_64
CPU op-mode(s): 32-bit, 64-bit //可以工作在32、64位环境下
Byte Order: Little Endian //小端字节序,ibm的powerpc是大端字节序
CPU(s): 24 //一个有24颗逻辑cpu
On-line CPU(s) list: 0-23 //在线的cpu为0~24,使cpu下线的方法是echo 0 > /sys/devices/system/cpu/cpuX/online
Thread(s) per core: 2 //超线程,每个cpu核运行2个线程
Core(s) per socket: 6 //每个物理cpu包含6个核,即12个逻辑cpu
Socket(s): 2 //主板上2个cpu插槽,共有2个物理cpu
NUMA node(s): 2 //有2个numa结点
Vendor ID: GenuineIntel
CPU family: 6
Model: 45
Stepping: 7
CPU MHz: 2000.121 //cpu主频2000MHz
BogoMIPS: 3999.47
Virtualization: VT-x // 支持虚拟化
L1d cache: 32K //L1数据cache 32KB
L1i cache: 32K //L1指令cache 32KB
L2 cache: 256K //L2 cache 256KB
L3 cache: 15360K //L3 cache 15MB
NUMA node0 CPU(s): 0,2,4,6,8,10,12,14,16,18,20,22 //node0 上的cpu序号
NUMA node1 CPU(s): 1,3,5,7,9,11,13,15,17,19,21,23
可以通过下图看出cpu内部的缓存分布:
在图中可以看出,每个socket即物理cpu,管理16GB的内存,并且每个物理cpu共享15MB的L3缓存。每个cpu核拥有单独的L1i、L1d、L2缓存,这个cpu核上的两个线程(超线程cpu核)共享这些缓存。
1. cache line
cpu cache line的概念可以参考:http://blog.csdn.net/zdl1016/article/details/8882092
cpu不会直接访问内存,都是通过将内存数据load到缓存中,再从cache间接获得数据。在64位的cpu上cache line的大小通常为64字节,也就是说每次cpu的cache会将64字节的数据从主存中加载到cache line中。但是,如果当程序的多个线程运行在不同的cpu核上,并且这几个线程要并发访问同一个全局结构体中的变量,就会导致SMP同步竞争、缓存失效等问题。
比如存在如下结构体
struct test{
int readNum;
int writeNum;
};
struct test testA;
在cpu0中,线程1对testA.readNum++,而在cpu1中,线程2对testA.writeNum++。线程1、线程2的每次操作,都会导致另外一个cpu核上对应的cache line无效,另外一个cpu需要重新从主存中将结构体load到cache中。
与之相比:
struct test{
int readNum;
char hole1[64];
int writeNum;
};
struct test testB;
这次,由于结构体中存在一个64字节的空洞,所以cpu0加载到缓存中的数据中,是不含有writeNum的,也就是无论线程0对testB.readNum如何操作,也影响不到cpu1中的cache的有效性。从而避免了cache频繁地从主存中读取数据
2. 绑核
对于cpu占用比较高的线程,可以使用绑核的手段将其与固定的cpu核绑定。由于进程在不同的核切换过程耗费cpu,并且,切换cpu也导致了之前cpu的cache中数据失效,在新的cpu核中,还要重新load数据的cache。所以对于性能要求很高的线程进行绑核,是非常必要的。
绑核的函数如下所示:
void setThreadAffinity(long _id)
{
long thread_id = (long)_id;
u_int numCPU = sysconf( _SC_NPROCESSORS_ONLN );
if(numCPU > 1) {
int s;
/* Bind this thread to a specific core */
cpu_set_t cpuset;
u_long core_id = thread_id % numCPU;
CPU_ZERO(&cpuset);
CPU_SET(core_id, &cpuset);
if((s = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset)) != 0)
printf("Error while binding thread %ld to core %ld: errno=%i\n",
thread_id, core_id, s);
else {
printf("Set thread %lu on core %lu/%u\n", thread_id, core_id, numCPU);
}
}
}
3. 多线程并发,也要利用好cache
在一般的概念里,多个线程跑在不同的cpu核上性能一定比跑在一个cpu核上性能要好,但是在实际运行环境中未必如此。
比如说典型的缓存程序场景中,一个线程作为生产者,向缓存中put数据,另外一个线程作为消费者,从缓存中读取数据并通过socket发送给其他客户端。由于只有同一个cpu核(或者同一个核上的2个超线程核)能够使用32KB的L1d、256KB的L2缓存,所以当生产者线程、消费者线程的步调高度一致的时候,就能最大限度的利用L1、L2缓存。如果生产者、消费者线程耗费的cpu很多,如果共存在一个cpu上会导致cpu占率100%,也可以退而求其次,将这两个线程部署在同一个numa node上,也就是同一个物理cpu上,这样就可以最大限度的利用L3 cache。
并且还要注意,很多情况下,cpu0、cpu1并不一定在一个物理cpu上,比如本文所示的图中,cpu 核0,2,4,6,8,10,12,14,16,18,20,22位于一个物理cpu中,cpu核1,3,5,7,9,11,13,15,17,19,21,23位于另外一个物理cpu中。
4. 利用好numa架构的特点
通常较新的服务器cpu都是numa架构的,当一台服务器中,是使用了2个物理cpu以后,就要在编程中考虑numa架构带来的影响。在多个cpu的numa环境中,cpu访问的内存分为近端内存、远端内存。比如在两个cpu的服务中,每个物理cpu都是一个numa node,每个numa node都直接管理一部分内存,有本numa node管理的内存就被称为近端内存,当cpu访问这种内存的开销较小时延较低;远端内存是指另外一个numa node管理的内存,当cpu访问远端内存时,就要通过QPI总线访问另一cpu,由另一个cpu读取内存,所以访问远端内存的开销较大,cpu占用较高。
为了解决这种问题,系统提供了一系列的numa api,通过numa api可以干预内存的分配,将线程所需的内存分配到近端内存区域。 最简单的用法是使用numa_setlocal_memory函数,这个函数的两个参数是内存的起始地址、长度,通过这个函数可以将这部分内存移动到近端内存区域,加速对这部分内存的访问。