Cache存储器
电脑中为高速缓冲存储器,是位于CPU和主存储器DRAM(Dynamic Random Access Memory)之间,规模较小,但速度很高的存储器,通常由SRAM(Static Random Access Memory 静态存储器)组成。它是位于CPU与内存间的一种容量较小但速度很高的存储器。CPU的速度远高于内存,当CPU直接从内存中存取数据时要等待一定时间周期,而Cache则可以保存CPU刚用过或循环使用的一部分数据,如果CPU需要再次使用该部分数据时可从Cache中直接调用,这样就避免了重复存取数据,减少了CPU的等待时间,因而提高了系统的效率。Cache又分为L1Cache(一级缓存)和L2Cache(二级缓存),L1Cache主要是集成在CPU内部,而L2Cache集成在主板上或是CPU上。
代码
/*
* 代码思路:创建一个连续内存块,进行连贯、大量、随机的有意义访问,要保证整块内存尽可能全部放入cache。当
* 内存被整块放入cache中时,访问速度会明显加快,直到有一个时间跳跃点,消耗时间增多,则这个跳跃点的存储容* 量大小即为cache大小
*/
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream> #include <random> #include <ctime> #include <algorithm> #define KB(x) ((size_t)(x) << 10) using namespace std; int main() { // 需要测试的数组的大小 vector<size_t> sizes_KB; for (int i = 1; i < 18; i++) { sizes_KB.push_back(1 << i); } random_device rd; // 伪随机数算法,计算更快,占用内存更少 mt19937 gen(rd()); for (size_t size : sizes_KB) { // 离散均匀分布类 uniform_int_distribution<> dis(0, KB(size) - 1); // 创建连续内存块 vector<char> memory(KB(size)); // 在内存中填入内容 fill(memory.begin(), memory.end(), 1); int dummy = 0; // 在内存上进行大量的随机访问并计时 clock_t begin = clock(); // 1<<25:将1左移25位,进行大量随机访问 for (int i = 0; i < (1 << 25); i++) { dummy += memory[dis(gen)]; } clock_t end = clock(); // 输出 double elapsed_secs = double(end - begin) / CLOCKS_PER_SEC; cout << size << " KB, " << elapsed_secs << "secs, dummy:" << dummy << endl; } }
运行结果:
测试结果并未看出什么,如果按照链接的跳跃点也是在1024kb,不过我电脑的大小是8M。
想要知道各级缓存的大小,可以查看任务管理器。
可以看到三级缓存是8M。怎么获取Cache的大小,可以使用CPUID命令,查看相应CPU的datasheet,但是会区分Intel和AMD,Intel的看CPUID的Spec,AMD的看CPU的datasheet就行了,里面会有CPUID命令。
然后用CPUID得到L1cache是64kb,L2cache是256kb,L3cache是8M。看起来和任务管理器里面的不一样,L1cache和L2cache明显有差异。那么到底谁对谁错呢,于是继续研究下去,使用了cpu-z的工具查看,仿佛明白了一点点。这个工具上显示的结果也是64,256,8192,貌似和CPUID命令得到的结果是一样的,然后仔细看了下去,翻到Cache那一页,发现L1cache和L2cache后面还有x4的字样,突然想到之前在哪篇文章里好像看到过CPU的每一个核里都会有cache,然后看了一下这个CPU的核数,刚好是四核。好像可以解释了,但是为什么L3没有x4呢,虽然有一定的猜测,但是还不确定,L1,L2,L3,L4级数越高,离CPU越远,会不会是L1,L2正好集成在CPU上,而L3集成在CPU外部呢,也是有这种可能的,不过目前还没有依据。
存储器是分层次的,离CPU越近的存储器。速度越快,每字节的成本越高,同时容量也因此越小。寄存器速度最快,离CPU最近,成本最高,所以个数容量有限,其次是高速缓存(缓存也是分级,有L1,L2等缓存),再次是主存(普通内存),再次是本地磁盘。
寄存器的速度最快,可以在一个时钟周期内访问,其次是高速缓存,可以在几个时钟周期内访问,普通内存可以在几十个或几百个时钟周期内访问。
存储器分级,利用的是局部性原理。我们可以以经典的阅读书籍为例。我在读的书,捧在手里(寄存器),我最近频繁阅读的书,放在书桌上(缓存),随时取来读。当然书桌上只能放有限几本书。我更多的书放在书架上(内存)。如果书架上没有的书,就去图书馆(磁盘)。我要读的书如果手里没有,那么去书桌上找,如果书桌上没有,去书架上找,如果书架上没有,去图书馆找。可以对应寄存器没有,则从缓存中取,缓存中没有,则从内存中取到缓存,如果内存中没有,则先从磁盘读入内存,再读入缓存,再读入寄存器。
cache分成多个组,每个组分成多个行,linesize是cache的基本单位,从主存向cache迁移数据都是按照linesize为单位替换的。比如linesize为32Byte,那么迁移必须一次迁移32Byte到cache。这个linesize比较容易理解,想想我们前面书的例子,我们从书架往书桌搬书必须以书为单位,肯定不能把书撕了以页为单位。书就是linesize。当然了现实生活中每本书页数不同,但是同个cache的linesize总是相同的。
所谓8路相连(8-way set associative) 的含义是指,每个组里面有8个行。
我们知道,cache的容量要远远小于主存,主存和cache肯定不是一一对应的,那么主存中的地址和cache的映射关系是怎样的呢?
拿到一个地址,首先是映射到一个组里面去。如何映射?取内存地址的中间几位来映射。
举例来说,data cache:32-KB,8-way set associative,64-byte line size
Cache总大小为32KB,8路组相连(每组有8个line),每个line的大小linesize为64Byte,OK,我们可以很轻易的算出一共有32K/8/64 = 64个组。
对于32位的内存地址,每个line有2^6 = 64Byte,所以地址的[0,5]区分line中的那个字节。一共有64个组。我们取内存地址中间6位来hash查找地址属于哪个组。即内存地址的[6,11]位来确定属于64组的哪一个组。组确定了之后,[12,31]的内存地址与组中8个line挨个比对,如果[12,31]位与某个line一致,并且这个line为有效,那么缓存命中。
cache分成三类:
1. 直接映射高速缓存,即每个组只有一个line,选中组之后不需要和组中的每个line比对,因为只有一个line。
2. 组相联高速缓存,这个就是我们前面介绍的cache。S个组,每个组E个line。
3. 全相联高速缓存,只有一个组,就是全相联。不用hash来确定组,直接挨个比对高位地址,来确定是否命中。可以想见这种方式不适合大的缓存。想想看,如果4M的大缓存,linesize为32Byte,采用全相联的话,就意味着4*1024*1024/32 = 128K个line挨个比较,来确定是否命中,这是多要命的事情。高速缓存立马成了低速缓存了。
描述一个cache需要以下参数:
1. cache的分级,L1 cache,L2 cache,L3 cache,级别越低,离CPU越近
2. cache的容量
3. cache的linesize
4. cache每组的行个数。
组的个数完全可以根据上面的参数计算出来,所以没有列出来。
Intel手册中用这样的句子来描述cache:
8-MB L3 Cache, 16-way set associative, 64-byte line size
如何获取cache的参数呢,需要用CPU指令,当eax为0x2的时候,cpuid指令获取到cache的参数。当然,具体的还是得看相应的spec,才会知道应该传什么值到什么寄存器,以及从什么寄存器里面读值出来,以及有效位是哪几位。