如何高效的利用CPU缓存

CPU缓存

我们知道CPU的缓存一般是由三级缓存构成,缓存离CPU越近,CPU访问缓存的速度就越快。如下图所示,每个核心都有自己的一、二级缓存,但三级缓存却是一颗 CPU 上所有核心共享的;程序执行时,会先将内存中的数据载入到共享的三级缓存中,再进入每颗核心独有的二级缓存,最后进入最快的一级缓存,之后才会被 CPU 使用。
在这里插入图片描述

CPU 访问一次内存通常需要 100 个时钟周期以上,而访问一级缓存只需要 4~5 个时钟周期,二级缓存大约 12 个时钟周期,三级缓存大约 30 个时钟周期。
如果 CPU 所要操作的数据或指令在缓存中,则直接读取,这称为缓存命中。命中缓存会带来很大的性能提升,因此,我们的代码优化目标是提升 CPU 缓存的命中率。虽然在冯诺依曼计算机体系结构中,代码指令与数据是放在一起的,但执行时却是分开进入指令缓存与数据缓存的,因此我们要分开来看二者的缓存命中率。

高效利用数据缓存

我们先来看看如下的代码的输出

uint64_t start = get_time_ms();
static int arrayss[10240][10240];
for (int i = 0; i < 10240; ++i)
{
	for (int j = 0; j < 10240; ++j)
	{
		arrayss[i][j] = 0;
	}
}
SCREEN_ERROR("take time 1: %lu", get_time_ms() - start);
start = get_time_ms();
for (int i = 0; i < 10240; ++i)
{
	for (int j = 0; j < 10240; ++j)
	{
		arrayss[j][i] = 0;
	}
}
SCREEN_ERROR("take time 2: %lu", get_time_ms() - start);
[ERROR] take time 1: 788
[ERROR] take time 2: 4519

可以看到第二段代码的运行效率是第一段的6倍,为什么差距那么大呢?我们知道在C/C++中二维数组在内存中是按行连续存放的。而CPU从内存中加载数据是按CACHE_LINE的大小从内存中加载数据的,典型的CACHE_LINE大小为64字节,可以通过如下的指令查看:

cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64

那么对于第一段代码,第一次访问arrayss[0][0]时,会一次性把后面的16个元素都加载到缓存中,那么接下来的16次元素访问都不需要从内存中加载,直到访问第17个元素,在一次性加载16个元素。而第二段代码每次访问元素都得从内存中加载16个元素,但只利用了其中的一个。显然第一段代码的缓存利用率提高了16倍。那为什么CPU要一次性加载CACHE_LINE字节的数据到缓存中呢?这主要是因为程序的局部性原理。程序的局部性原理指的是在一段时间内程序的执行会限定在一个局部范围内。这里的“局部性”可以从两个方面来理解,一个是时间局部性,另一个是空间局部性。时间局部性指的是程序中的某条指令一旦被执行,不久之后这条指令很可能再次被执行;如果某条数据被访问,不久之后这条数据很可能再次被访问。而空间局部性是指某块内存一旦被访问,不久之后这块内存附近的内存也很可能被访问。

避免不能高效利用数据缓存

同样先看一段代码

struct FalseSharing
{
volatile int takeIndex;
volatile int putIndex;
}
FalseSharing falseShare = {0, 0};
void falseSharingFunc1()
{
++falseShare.takeIndex;
}
void falseSharingFunc2()
{
++falseShare.putIndex;
}

由于CPU的Cache是按照CACHE_LINE管理的,当 CPU 从内存中加载 takeIndex 的时候,会同时将 putIndex 也加载进 Cache。如下图所示,假设falseSharingFunc1 运行在 CPU1 上,falseSharingFunc2运行在CPU2上面且他们同时在运行,当falseSharingFunc1 执行加1操作时,falseSharingFunc2也执行了加1操作,由于他们各自缓存到了各自的CPU且处于同一个缓存行中,那么当CPU2执行加1时,由于CPU2的缓存行已经失效了,故需要从内存重新加载,从而导致不能高效的利用缓存。这就是伪共享。伪共享指的是由于共享缓存行导致缓存无效的场景。
在这里插入图片描述

那么我们怎么可以避免伪共享呢?方案很简单,每个变量独占一个缓存行、不共享缓存行就可以了,具体技术是缓存行填充。我们可以把上面的结构体改成:

struct FalseSharing
{
char padding[60];
volatile int takeIndex;
char padding[60];
volatile int putIndex;
char padding[60];
}

高效利用指令缓存

同样的先看一段代码

#define __SIZE__    (1024000)
static int arrays[__SIZE__];
int num = 0;
for (int i = 0; i < __SIZE__; ++i)
{
	arrays[i] = get_rand(256);
}
uint64_t start = get_time_ms();
for (int i = 0; i < __SIZE__; ++i)
{
	if (arrays[i] < 128)
	{
		++num;
	}
}
SCREEN_ERROR("take time 1: %lu, num: %d", get_time_ms() - start, num);
for (int i = 0; i < __SIZE__ /2; ++i)
{
	arrays[i] = get_rand(128);
}
for (int i = __SIZE__ / 2; i < __SIZE__; ++i)
{
	arrays[i] = get_rand(128) + 128;
}
num = 0;
start = get_time_ms();
for (int i = 0; i < __SIZE__; ++i)
{
	if (arrays[i] < 128)
	{
		++num;
	}
}
SCREEN_ERROR("take time 2: %lu, num: %d", get_time_ms() - start, num);
[ERROR] take time 1: 8, num: 511563
[ERROR] take time 2: 2, num: 512000

可以看出第二段代码的执行效率是第一段的4倍,为什么差距会那么大呢?这是因为循环中有大量的 if 条件分支,而 CPU含有分支预测器。当代码中出现 if、switch 等语句时,意味着此时至少可以选择跳转到两段不同的指令去执行。如果分支预测器可以预测接下来要在哪段代码执行(比如 if 还是 else 中的指令),就可以提前把这些指令放在缓存中,CPU 执行时就会很快。当数组中的元素完全随机时,分支预测器无法有效工作,而当数组前一半的元素都小于128时,分支预测器会动态地根据历史命中数据对未来进行预测,命中率就会非常高。

缓存命中率查看

那么我们怎么查看CPU的缓存命中率呢?我们可以使用linux的perf stat命令查看进程的cpu使用率。
执行 perf stat 可以统计出进程运行时的系统信息。通过 -e 选项指定要统计的事件。

cache-misses	缓存未命中
cache-references 读取缓存次数
L1-dcache-load-misses 一级缓存未命中
L1-dcache-loads 一级缓存读取缓存次数
L1-icache-loads 一级缓存读取缓存次数
L1-icache-load-misses 一级缓存中指令的未命中情况
branch-loads 分支预测的次数
branch-load-misses 分支预测失败的次数

cache-misses 与 cache-references两者相除就是缓存的未命中率,用 1 相减就是命中率。其他也类似。

CPU亲和性

操作系统提供了将进程或者线程绑定到某一颗 CPU 上运行的能力。如 Linux 上提供了 sched_setaffinity 方法实现这一功能

一个CPU的亲合力掩码用一个cpu_set_t结构体来表示一个CPU集合,下面的几个宏分别对这个掩码集进行操作:
CPU_ZERO() 清空一个集合
CPU_SET()与CPU_CLR()分别对将一个给定的CPU号加到一个集合或者从一个集合中去掉.
CPU_ISSET()检查一个CPU号是否在这个集合中
头文件 sched.h
sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数设置进程为pid的这个进程,让它运行在mask所设定的CPU上。
如果pid的值为0,则表示指定的是当前进程,使当前进程运行在mask所设定的那些CPU上。
sched_getaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数获得pid所指示的进程的CPU位掩码,并将该掩码返回到mask所指向的结构中。如果pid的值为0,表示的是当前进程。

那为什么需要绑定CPU呢?现在操作系统都是按时间片给每个进程分配时间运行,当进程的时间片用完了,就会切换给其他进程使用。若进程 A 在时间片 1 里使用 CPU 核心 1,自然也填满了核心 1 的一、二级缓存,当时间片 1 结束后,操作系统会让进程 A 让出 CPU,基于效率并兼顾公平的策略重新调度 CPU 核心 1,以防止某些进程饿死。如果此时 CPU 核心 1 繁忙,而 CPU 核心 2 空闲,则进程 A 很可能会被调度到 CPU 核心 2 上运行,这样,即使我们对代码优化得再好,也只能在一个时间片内高效地使用 CPU 一、二级缓存了,下一个时间片便面临着缓存效率的问题。
当多线程同时执行密集计算,且 CPU 缓存命中率很高时,如果将每个线程分别绑定在不同的 CPU 核心上,性能便会获得非常可观的提升。perf 工具也提供了 cpu-migrations 事件,它可以显示进程从不同的 CPU 核心上迁移的次数。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: CPU利用率是指计算机中央处理器(CPU)在某一时间段内执行任务的效率和负载。在Winform应用程序中,我们可以通过一些方法来监测和优化CPU利用率。 首先,我们可以使用性能监视器工具来查看CPU利用率。在Windows操作系统中,可以通过任务管理器的"性能"选项卡来实时监测CPU利用率。如果发现CPU利用率过高,说明程序在执行过程中占用了较多的CPU资源,可能存在性能瓶颈。此时我们可以通过优化代码、合理利用多线程等方法降低CPU的负载。 其次,我们可以通过优化算法来减少程序的计算量和时间复杂度。可以使用更高效的算法或数据结构来替代原有的低效实现,从而减少CPU的工作量。 此外,开发者在编写Winform应用程序时,可以合理利用异步操作和多线程编程。通过将耗时的操作放在后台线程中执行,可以使主线程的CPU利用率降低,提高用户体验。 还有一些其他的方法可以帮助我们提高CPU利用率,例如使用缓存技术减少IO操作、避免不必要的循环或重复计算、使用合适的编译器选项进行编译优化等。 总之,通过合理的优化和调整,我们可以提高Winform应用程序的CPU利用率,提升程序的性能和响应速度。 ### 回答2: CPU利用率是指计算机CPU的工作效率和负载情况。在WinForm程序中,可以通过以下方法来获取和监控CPU利用率: 1. 使用PerformanceCounter类: PerformanceCounter类是.NET框架提供的一个用于性能监控的类。可以通过该类来获取计算机的各项性能指标,包括CPU利用率。在WinForm中,可以使用PerformanceCounter类来获取CPU利用率,并将其显示在界面上,实现对CPU利用率的实时监控。 2. 使用WMI查询: WMI(Windows Management Instrumentation)是一种用于对Windows系统进行管理和监控的技术。通过WMI,我们可以使用查询语句来获取各种系统信息,包括CPU利用率。在WinForm中,我们可以使用WMI查询来获取CPU利用率,并将其显示在界面上。 3. 使用任务管理器: 任务管理器是Windows系统内置的一个实用程序,可以用于监控系统的各项性能指标,包括CPU利用率。在WinForm中,我们可以使用System.Diagnostics命名空间中的Process类来启动任务管理器,并获取其中的CPU利用率信息。然后将这些信息显示在界面上,实现对CPU利用率的监控。 总结起来,通过PerformanceCounter类、WMI查询或者任务管理器,我们可以获取和监控WinForm程序中的CPU利用率。可以将CPU利用率的信息实时显示在界面上,让用户了解系统在运行过程中的负载情况,从而做出相应的优化和调整。 ### 回答3: CPU利用率是指计算机中心处理器(CPU)在一定时间内运行程序的效率。利用率高表示CPU使用效率高,无暇闲置;而利用率低则表示CPU性能浪费、处理速度较慢。 在WinForm(Windows窗体应用程序)中,可以通过以下方法获取CPU利用率: 1. 使用PerformanceCounter类:在C#中,可以使用PerformanceCounter类来获取CPU利用率。首先,需要引入System.Diagnostics命名空间,然后实例化PerformanceCounter类,并指定计数器的名称为"% Processor Time"。通过调用NextValue()方法获取当前的CPU利用率值。 示例代码如下: ``` using System.Diagnostics; PerformanceCounter cpuCounter; public Form1() { InitializeComponent(); cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); } private void timer1_Tick(object sender, EventArgs e) { float cpuUsage = cpuCounter.NextValue(); label1.Text = "CPU利用率:" + cpuUsage.ToString() + "%"; } ``` 2. 使用WMI管理对象:可通过Windows Management Instrumentation(WMI)获取系统信息。在C#中,可以使用ManagementObjectSearcher类和ManagementObject类来查询计算机硬件信息,包括CPU利用率。 示例代码如下: ``` using System.Management; ManagementObjectSearcher searcher = new ManagementObjectSearcher("select * from Win32_PerfFormattedData_PerfOS_Processor where Name='_Total'"); foreach (ManagementObject obj in searcher.Get()) { float cpuUsage = Convert.ToSingle(obj["PercentProcessorTime"]); label1.Text = "CPU利用率:" + cpuUsage.ToString() + "%"; } ``` 以上是几种获取CPU利用率的方法。根据实际需求和个人喜好,可以选择适合自己的方式来实现对CPU利用率的监控。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值