参考
- Cache organization — L1, L2 and L3 cache
- Intel 64 and IA-32 Architectures Optimization Reference Manual .- chapter 9
- JVM && Cache
CPU的cache
CPU的cache是一种高速存储器,位于CPU内部,用于临时存储频繁使用的数据和指令。它的存在主要是为了解决CPU与主存之间的速度差异,提高计算机的性能。
CPU的缓存工作原理如下:
-
缓存命中与缓存失效:
当CPU需要访问某个内存地址时,它首先检查该地址是否在缓存中。如果在缓存中找到了该地址对应的数据,则称为缓存命中,CPU可以直接从缓存中读取数据,这样可以大大提高访问速度。
如果地址不在缓存中,则称为缓存失效,CPU需要从主存中读取数据,并将其加载到缓存中。 -
局部性原理:CPU缓存利用了
程序的局部性原理
,即程序在一段时间内倾向于访问相同或附近的内存地址。这是由于程序的指令和数据通常以连续的方式存储在主存中。 -
缓存层次结构:CPU的缓存通常分为多级缓存,如L1、L2和L3缓存。这些缓存按照容量和访问速度递减的顺序排列,L1缓存最小但最快,L3缓存最大但最慢。CPU在访问内存时,首先查找L1缓存,如果数据不在L1缓存中,则继续查找L2缓存,以此类推。
-
缓存行:CPU的缓存以缓存行的方式组织数据。缓存行是内存中一块连续的数据块,
通常大小为64字节(B)或128字节(B)
。当CPU需要访问内存中的某个地址时,它会将该地址所在的缓存行加载到缓存中,以便快速访问。
-
缓存替换策略:当缓存已满,且需要将新的数据加载到缓存中时,需要选择一个缓存行来替换。常见的缓存替换策略有最近最少使用(LRU)、最不经常使用(LFU)和随机替换等。
图解说明
L1、L2和L3缓存之间的性能差异
access latency | CPU周期 | 时间(单位ns) |
---|---|---|
寄存器 | 1 cycle | |
L1 Cache | ~3-4 cycles | ~0.5-1 ns |
L2 Cache | ~10-20 cycles | ~3-7 ns |
L3 Cache | ~40-45 cycles | ~15 ns |
内存 | ~120-240 cycles | ~60-120ns |
cache和内存的映射
直接相联方式 : 主存储器中一块只能映象到Cache的一个特定的块中。
-
地址变换过程:用主存地址中的块号B去访问目录存储器, 把读出来的区号与主存地址中的区号E进行比较, 比较结果相等,有效位为1,则Cache命中,可以直接用块号及块内地址组成的缓冲地址到缓存中取数;比较结果不相等,有效位为1, 可以进行替换,如果有效位为0,可以直接调入所需块。
-
例:32位计算机,cache容量16kb( 2 14 2^{14} 214b,cache地址有14位),主存0x12345678装入地址,将0x12345678转为二进制选择后14位即可。
全相联方式:主存的任意一块可以映象到Cache中的任意一块
组相联映象方式
(1) 主存和Cache按同样大小划分成块。
(2) 主存和Cache按同样大小划分成组。
(3) 主存容量是缓存容量的整数倍,将主存空间按缓冲区的大小分成区,主存中每一区的组数与缓存的组数相同。
(4) 当主存的数据调入缓存时,主存与缓存的组号应相等,也就是各区中的某一块只能存入缓存的同组号的空间内,但组内各块地址之间则可以任意存放, 即从主存的组到Cache的组之间采用直接映象方式
;在两个对应的组内部采用全相联映象
方式。
块的替换规则
直接相联不需要替换策略,因为其本身的映射方式已经决定了一个主存块应该放进哪个 Cache Line。而组相联和全相联是需要替换策略的,常见的替换策略如下:
- 随机替换
- 先进先出
- 最近最少使用(LRU)
块写策略
cache写命中
写缺失策略
缓存命中率是指在访问缓存中的数据时,所请求的数据是否能够在缓存中找到的比率。它是衡量缓存性能的重要指标之一。
提高缓存命中率的软件优化方法如下:
-
提高局部性:局部性是指程序在一段时间内对某些数据的访问集中在一定的地址范围内。通过优化算法和数据结构,可以提高程序的局部性,从而增加缓存命中率。
-
数据预取:通过预测程序的访问模式,
提前将可能被访问的数据加载到缓存中
,减少缓存未命中的情况。 -
数据对齐:将数据按照缓存行的大小进行对齐,可以减少缓存未命中的情况。
-
减少冲突:冲突是指多个数据映射到同一个缓存位置,导致缓存未命中。通过增加缓存的大小、优化缓存的映射方式等方法,可以减少冲突,提高缓存命中率。
-
缓存替换算法:选择合适的缓存替换算法,可以在缓存未命中时选择最有可能被访问的数据替换出来,提高缓存命中率。
-
数据压缩:通过压缩数据,可以减少数据在缓存中的占用空间,增加缓存的容量,从而提高缓存命中率。
-
减少伪共享(false sharing) https://www.codeproject.com/Articles/85356/Avoiding-and-Identifying-False-Sharing-Among-Threa
examples
cache利用率
数据对齐(int 4 byte ,char 1 byte)
按照行访问
falseSharing
- https://github.com/MJjainam/falseSharing
- false sharing(伪共享) 及 c代码实现
- Int array[100]数组,
线程1对0号位置的元素修改,实现累加100M次,线程2对1号位置的元素修改,实现累加100M次
,由于两个线程修改的数据共享同一cache line,所以造成false sharing;如何改进呢?将线程2对16号位置的元素做修改,因为一行cache line占64字节,相当于16个int类型元素,所以进行padding,达到两个线程用两个cache line的目的,即可消除问题。 - Struct test{}结构体,里面包含int num_int,long num_long,int arr[16] 三种数据类型,这三种数据类型的位置摆放也影响了性能,如果按照这个顺序排列,实现对 num_int和num_long数据类型的累加操作并修改,一个线程写num_int,另一个写num_long,由于在同一cache line,存在false sharing,
而我们把arr数组放到 num_long前面,将要修改的两个数据放到不同的cache line中,即可消除问题,因为arr数组占据了16个字节,跨越到下一个cache line了
。 - 感觉和GPU的bank冲突有些类似
CG
- https://stackoverflow.com/questions/8547778/why-are-elementwise-additions-much-faster-in-separate-loops-than-in-a-combined-l
- https://zaguan.unizar.es/record/47388/files/TAZ-TFG-2015-2519.pdf
- 计组3.5 全相联、直接映射、组相联、Cache命中率
- 如何提高cache的性能?
- https://www.cnblogs.com/luoyinjie/p/10063432.html
- https://zhuanlan.zhihu.com/p/641241366
- https://zzqcn.github.io/perf/cpu_cache.html#cpu-cache-the-need-to-know
simd
在过去的十年里,处理器的速度一直在提高。内存访问速度以较慢的速度增加。由此产生的差异使得以两种方式之一调整应用程序变得很重要:(a)大多数数据访问都是从处理器缓存中完成的,或者(b)有效地屏蔽内存延迟,以尽可能多地利用峰值内存带宽。
硬件预取机制是微体系结构中的增强功能,有助于实现后一个方面,当与软件调优相结合时,它将是最有效的。
如果所需的数据可以从处理器缓存中提取,或者如果内存流量可以有效地利用硬件预取,那么大多数应用程序的性能都可以得到显著提高。
在需要数据之前将数据带入处理器的标准技术涉及额外的编程,这可能很难实现,并且可能需要特殊步骤来防止性能下降。
“数据流单指令多数据扩展指令集”(Streaming SIMD Extensions)通过提供各种预取指令来解决这个问题。SIMD引入了各种非临时存储指令。SSE2扩展了这一点支持新的数据类型,还引入了对32位整数寄存器的非临时存储支持。
本章重点介绍:
- 硬件预取机制、软件预取和可缓存性说明–讨论允许您影响应用程序中数据缓存的微体系结构功能和说明。
- 使用硬件预取、软件预取和可缓存性指令的内存优化–讨论使用上述指令实现内存优化的技术。
- 使用确定性缓存参数来管理缓存层次结构。
#include <time.h>
struct timespec before, after;
long elapsed_nsecs;
int main(){
// struct DATB
// {
// int a;
// int b;
// };
// DATB pMyDatb[10*1024];
// clock_gettime(CLOCK_REALTIME, &before);
// for(int j=0;j<100000;j++)
// for (long i=0; i<10*1024; i++) {
// pMyDatb[i].a = pMyDatb[i].b;
// }
// clock_gettime(CLOCK_REALTIME, &after);
// elapsed_nsecs = (after.tv_sec - before.tv_sec) * 1000000000 +(after.tv_nsec - before.tv_nsec);
// printf("elapsed : %ld",elapsed_nsecs);//1597293426
struct DATA
{
int a;
int b;
int c;
int d;
};
DATA pMyData[10*1024];
long elapsed_nsecs;
clock_gettime(CLOCK_REALTIME, &before);
for(int j=0;j<100000;j++)
for (long i=0; i<10*1024; i++) {
pMyData[i].a = pMyData[i].b;
}
clock_gettime(CLOCK_REALTIME, &after);
elapsed_nsecs = (after.tv_sec - before.tv_sec) * 1000000000 +
(after.tv_nsec - before.tv_nsec);
printf("elapsed : %ld",elapsed_nsecs);// elapsed : 1620390389
return 0;
}