多核环境下cache line的测试
前阵子接触到一道关于数组内部链表(多用于内存池技术)的数据结构的题, 这种数据结构能够比普通链表在cache中更容易命中, 理由很简单, 就是因为其在地址上是连续的(=.=!), 借这个机会, 就对cpu cache进行了一个研究, 今天做一个简单的分享, 首先先来普及一下cpu cache的知识, 这里的cache是指cpu的高速缓存. 在我们程序员看来, 缓存是一个透明部件. 因此, 程序员通常无法直接干预对缓存的操作. 但是, 确实可以根据缓存的特点对程序代码实施特定优化, 从而更好地利用高速缓存.
高速缓存的置换策略会尽可能地将 访问频繁的数据放入cache中, 这是一个动态的过程, 所以cache中的数据不会一直不变. 目前一般的机器的cpu cache可分为一级缓存和二级缓存. 一级缓存更靠近cpu, 速度比二级缓存更快. 二级缓存比一级缓存速度更慢, 容量更大, 主要就是做一级缓存和内存之间数据临时交换的地方用.
这两者和RAM在空间和效率上的关系如下:
L1 Cache —> L2 Cache —> RAM
————> 容量递增 ————>
————> 速度递减 ————>
—–> CPU访问优先级递减 —–>
在linux系统中, 我们可以使用cat /proc/cpuinfo 来获知机器的cpu和核数.
而cpu cache的信息, 我们通过dmesg | grep cache来获知.
例如:
CPU: L1 I Cache: 64K (64 bytes/line), D cache 64K (64 bytes/line)
CPU: L1 I Cache: 64K (64 bytes/line), D cache 64K (64 bytes/line)
说明我这台机器有两个处理器, 并只有一级缓存, 大小为 64K, 缓存行/快 大小为64 bytes.
由于不同的处理器之间都具有自己 的高速缓存, 所以当两个cpu的cache中都存有数据a, 那么就有可能需要进行同步数据, 而cache之间同步数据的最小单元为cache行大小, 可以把一个cache想象成一张表, 表的每一行都是64bytes(假设), 当cpu被告知cache第一行的第一个byte为脏数据时, cpu会将第一行都进行同步.
例如以下场景:
CPU1读取了数据a(假设a小于cache行大小),并存入CPU1的高速缓存.
CPU2也读取了数据a,并存入CPU2的高速缓存.
CPU1修改了数据a, a被放回CPU1的高速缓存行. 但是该信息并没有被写入RAM.
CPU2访问a, 但由于CPU1并未将数据写入RAM, 导致了数据不同步.
为了解决这个问题, 芯片设计者制定了一个规则. 当一个CPU修改高速缓存行中的字节时, 计算机中的其它CPU会被通知, 它们的高速缓存将视为无效. 于是, 在上面的情况下, CPU2发现自己的高速缓存中数据已无效, CPU1将立即把自己的数据写回RAM, 然后CPU2重新读取该数据. 这样就完成了一次两个cpu之间cache的同步.
为了测试上述场景, 我编写了如下程序进行测试:
1 #define EXEC_COUNT (100 * 1000 * 1000)
2
3 struct bits_t
4 {
5 int a;
6 char placeholder[64];
7 int b;
8 };
9
10 struct bits_t bits;
11
12 int which_cpu(const char* prefix_)
13 {
14 #ifdef ENABLE_WHCIH_CPU
15 cpu_set_t cur_cpu;
16 CPU_ZERO(&cur_cpu);
17 if (sched_getaffinity(0, sizeof(cur_cpu), &cur_cpu) == -1)
18 {
19 printf("warning: cound not get cpu affinity, continuing...n");
20 return -1;
21 }
22 int num = sysconf(_SC_NPROCESSORS_CONF);
23 for (int i = 0; i < num; i++)
24 {
25 if (CPU_ISSET(i, &cur_cpu))
26 {
27 printf("[%s] this process %d is running processor : %dn", prefix_, getpid(), i);
28 }
29 }
30 #endif
31
32 return 0;
33 }
34
35 int set_cpu(int cpu_id_)
36 {
37 #ifdef ENABLE_SET_CPU
38 cpu_set_t mask;
39 CPU_ZERO(&mask);
40 CPU_SET(cpu_id_, &mask);
41 if (sched_setaffinity(0, sizeof(mask), &mask) == -1)
42 {
43 printf("warning: could not set CPU affinity, continuing...n");
44 return -1;
45 }
46 #endif
47
48 return 0;
49 }
50
51 void* thd_func1(void* arg_)
52 {
53 set_cpu(0);
54 which_cpu("thread 1 start");
55 timeval begin_tv;
56 gettimeofday(&begin_tv, NULL);
57
58 for (int i = 0; i < EXEC_COUNT; i++)
59 {
60 bits.a += 1;
61 int a = bits.a;
62 }
63
64 timeval end_tv;
65 gettimeofday(&end_tv, NULL);
66 printf("thd1 perf:[%lu]usn", (end_tv.tv_sec * 1000 * 1000 + end_tv.tv_usec) - (begin_tv.tv_sec * 1000 * 1000 + begin_tv.tv_usec));
67 which_cpu("thread 1 end");
68
69 return NULL;
70 }
71
72 void* thd_func2(void* arg_)
73 {
74 set_cpu(1);
75 which_cpu("thread 2 start");
76 timeval begin_tv;
77 gettimeofday(&begin_tv, NULL);
78
79 for (int i = 0; i < EXEC_COUNT; i++)
80 {
81 bits.b += 2;
82 int b = bits.b;
83 }
84
85 timeval end_tv;
86 gettimeofday(&end_tv, NULL);
87 printf("thd2 perf:[%lu]usn", (end_tv.tv_sec * 1000 * 1000 + end_tv.tv_usec) - (begin_tv.tv_sec * 1000 * 1000 + begin_tv.tv_usec));
88 which_cpu("thread 2 end");
89
90 return NULL;
91 }
92
93 int main(int argc_, char* argv_[])
94 {
95 int num = sysconf(_SC_NPROCESSORS_CONF);
96 printf("system has %d processor(s).n", num);
97 cpu_set_t cpu_mask;
98 cpu_set_t cur_cpu_info;
99
100 memset((void*)&bits, 0, sizeof(bits_t));
101 set_cpu(0);
102 which_cpu("main thread");
103
104 pthread_t pid1;
105 pthread_create(&pid1, NULL, thd_func1, NULL);
106
107 pthread_t pid2;
108 pthread_create(&pid2, NULL, thd_func2, NULL);
109
110 pthread_join(pid1, NULL);
111 pthread_join(pid2, NULL);
112
113 return 0;
114 }
该程序中会创建两个线程, 分别对全局变量bits的a和b成员进行1亿次加法操作.
在这里我分别针对四种情况进行了测试 -
1. 两个线程分别跑在不同的cpu上, bits_t结构体没有placeholder这64个填充字节.
2. 两个线程分别跑在不同的cpu上, bits_t结构体有placeholder这64个填充字节.
3. 两个线程分别跑在相同的cpu上, bits_t结构体没有placeholder这64个填充字节.
4. 两个线程分别跑在相同的cpu上, bits_t结构体有placeholder这64个填充字节.
程序可以通过set_cpu函数来将线程绑定到指定的cpu上去.
为了大家阅读的方便, 我已将测试结果报告整理成以下四个表格.
情况一测试结果:
线程id |
CPU绑定 |
有无placeholder |
平均耗时(微妙) |
---|---|---|---|
1 |
cpu0 |
无 |
2186931 |
2 |
cpu1 |
无 |
2033496 |
情况二测试结果:
线程id |
CPU绑定 |
有无placeholder |
平均耗时(微妙) |
---|---|---|---|
1 |
cpu0 |
有 |
402144 |
2 |
cpu1 |
有 |
392745 |
我们先来看情况一和情况二的结果 对比, 显然, 后者要比前者效率高得多的多, 可以验证在有 placeholder填充字节之后, bit_t的a和b域被划分到了cache的不同两行, 所以当在cpu0执行的线程1修改a后, cpu1在读b时, 不需要去同步cache. 而情况一因为a和b在cache中的同一行, 导致两个cpu要互相进行大量的cache行同步.
情况三测试结果:
线程id |
CPU绑定 |
有无placeholder |
平均耗时(微妙) |
---|---|---|---|
1 |
cpu0 |
无 |
716056 |
2 |
cpu0 |
无 |
686804 |
情况四测试结果:
线程id |
CPU绑定 |
有无placeholder |
平均耗时(微妙) |
---|---|---|---|
1 |
cpu0 |
有 |
761421 |
2 |
cpu0 |
有 |
884969 |
可以看出, 情况三和四, 因为两个线程运行在同一个cpu上, 有和没有placeholder填充字节在性能上几乎没有什么区别, 因为不存在cache之间行同步的问题, 但是由于是一个cpu在调度切换两个线程, 所以要比情况一慢一点.
从上面测试结果看来, 某些特定情况下, 对于cache的优化还是很重要的, 但是也不能一味地为了追求性能都将所有共享数据加入填充字节, 毕竟cache就那么大, 如果不是某些特定的读写非常频繁的场景下, 没有必要这么做.
PS: 由于不同的硬件架构体系之间会有差别, 例如某些硬件架构同一个cpu下的两个物理核之间共享cache, 所以测试时要试具体环境而定.
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Java Lombok 常用注解
- Python 基础 安装 简单的输入输出 运行一个py程序
- Python 基础 数据类型 变量常量
- Java 快速排序 关于起始方向的选择问题 为什么一定要从右边开始
- Java 使用异或进行数组元素交换时的坑 返回0的原因
- Spring BindingResult获取不到结果可能的原因之一 参数顺序 没有紧挨着校验参数
- 残差收缩网络:一种深度学习故障诊断算法
- Solr学习笔记 - 关于近实时搜索
- Solr学习笔记 - 关于timeAllowed
- Solr学习笔记 - 关于cache
- PG密码安全
- 如何利用Terraform工具编排管理TcaplusDB
- mysql隐式转换造成的查询结果不正确案例
- 【TBase开源版测评】体验安装
- 【Golang】go get遇到git fetch-pack: expected shallow list