多核环境下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 : %d\n", 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]us\n", (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]us\n", (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, 所以测试时要试具体环境而定.
 
来源:http://www.cppthinker.com/cpp/9/cpu_cache/







本文转自夏雪冬日博客园博客,原文链接:http://www.cnblogs.com/heyonggang/archive/2012/12/11/2813005.html,如需转载请自行联系原作者

目录
相关文章
|
2月前
|
机器学习/深度学习 人工智能 算法
BALROG:基准测试工具,用于评估 LLMs 和 VLMs 在复杂动态环境中的推理能力
BALROG 是一款用于评估大型语言模型(LLMs)和视觉语言模型(VLMs)在复杂动态环境中推理能力的基准测试工具。它通过一系列挑战性的游戏环境,如 NetHack,测试模型的规划、空间推理和探索能力。BALROG 提供了一个开放且细粒度的评估框架,推动了自主代理研究的进展。
48 3
BALROG:基准测试工具,用于评估 LLMs 和 VLMs 在复杂动态环境中的推理能力
|
2月前
|
缓存 Ubuntu Linux
Linux环境下测试服务器的DDR5内存性能
通过使用 `memtester`和 `sysbench`等工具,可以有效地测试Linux环境下服务器的DDR5内存性能。这些工具不仅可以评估内存的读写速度,还可以检测内存中的潜在问题,帮助确保系统的稳定性和性能。通过合理配置和使用这些工具,系统管理员可以深入了解服务器内存的性能状况,为系统优化提供数据支持。
54 4
|
2月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
224 1
|
2月前
|
编解码 安全 Linux
网络空间安全之一个WH的超前沿全栈技术深入学习之路(10-2):保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali——Liinux-Debian:就怕你学成黑客啦!)作者——LJS
保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali以及常见的报错及对应解决方案、常用Kali功能简便化以及详解如何具体实现
|
4月前
|
JavaScript 测试技术 Windows
vue配置webpack生产环境.env.production、测试环境.env.development(配置不同环境的打包访问地址)
本文介绍了如何使用vue-cli和webpack为Vue项目配置不同的生产和测试环境,包括修改`package.json`脚本、使用`cross-env`处理环境变量、创建不同环境的`.env`文件,并在`webpack.prod.conf.js`中使用`DefinePlugin`来应用这些环境变量。
236 2
vue配置webpack生产环境.env.production、测试环境.env.development(配置不同环境的打包访问地址)
|
3月前
|
分布式计算 Hadoop 大数据
大数据体系知识学习(一):PySpark和Hadoop环境的搭建与测试
这篇文章是关于大数据体系知识学习的,主要介绍了Apache Spark的基本概念、特点、组件,以及如何安装配置Java、PySpark和Hadoop环境。文章还提供了详细的安装步骤和测试代码,帮助读者搭建和测试大数据环境。
93 1
|
3月前
|
前端开发 测试技术 程序员
在工作中会涉及到的几个环境(概念补充) 办公环境、开发环境、测试环境、线下环境、线上环境/生产环境都是什么,他们之间的关系?
本文解释了在职场中可能会接触到的不同环境,包括办公环境、开发环境、测试环境和生产环境(线上环境),以及它们之间的关系和重要性。
121 1
|
23天前
|
监控 JavaScript 测试技术
postman接口测试工具详解
Postman是一个功能强大且易于使用的API测试工具。通过详细的介绍和实际示例,本文展示了Postman在API测试中的各种应用。无论是简单的请求发送,还是复杂的自动化测试和持续集成,Postman都提供了丰富的功能来满足用户的需求。希望本文能帮助您更好地理解和使用Postman,提高API测试的效率和质量。
80 11
|
2月前
|
JSON Java 测试技术
SpringCloud2023实战之接口服务测试工具SpringBootTest
SpringBootTest同时集成了JUnit Jupiter、AssertJ、Hamcrest测试辅助库,使得更容易编写但愿测试代码。
72 3
|
3月前
|
JSON 算法 数据可视化
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
这篇文章是关于如何通过算法接口返回的目标检测结果来计算性能指标的笔记。它涵盖了任务描述、指标分析(包括TP、FP、FN、TN、精准率和召回率),接口处理,数据集处理,以及如何使用实用工具进行文件操作和数据可视化。文章还提供了一些Python代码示例,用于处理图像文件、转换数据格式以及计算目标检测的性能指标。
90 0
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)