CPU高速缓存与极性代码设计

摘要:CPU内置少许的高速缓存的重要性不言而喻,在体积、成本、效率等因素下产生了当今用到的计算机的存储结构。
  1. 介绍
  2. cpu缓存的结构
  3. 缓存的存取与一致
  4. 代码设计的考量
  5. 最后

CPU频率太快,其处理速度远快于存储介质的读写。所以,致使CPU资源的浪费,须要有效解决IO速度和CPU运算速度之间的不匹配问题。芯片级高速缓存可大大减小之间的处理延迟。CPU制造工艺的进步使得在比之前更小的空间中安装数十亿个晶体管,如此可为缓存留出更多空间,使其尽量地靠近核心。html

CPU内置少许的高速缓存的重要性不言而喻,在体积、成本、效率等因素下产生了当今用到的计算机的存储结构。java

1. 介绍

计算机的内存具备基于速度的层次结构,CPU高速缓存位于该层次结构的顶部,是介于CPU内核和物理内存(动态内存DRAM)之间的若干块静态内存,是最快的。它也是最靠近中央处理的地方,是CPU自己的一部分,通常直接跟CPU芯片集成。算法

CPU计算:程序被设计为一组指令,最终由CPU运行。数组

装载程序和数据,先从最近的一级缓存读取,若有就直接返回,逐层读取,直至从内存及其它外部存储中加载,并将加载的数据依次放入缓存。缓存

高速缓存中的数据写回主存并不是当即执行,写回主存的时机:sass

1.缓存满了,采用先进先出或最久未使用的顺序写回;服务器

2.#Lock信号,缓存一致性协议,明确要求数据计算完成后要立马同步回主存。网络

2. CPU缓存的结构

现代的CPU缓存结构被分为多处理、多核、多级的层次。数据结构

2.1 多级缓存结构

分为三个主要级别,即L1,L2和L3。离CPU越近的缓存,读取效率越高,存储容量越小,造价越高。多线程

L1高速缓存是系统中存在的最快的内存。就优先级而言,L1缓存具备CPU在完成特定任务时最可能须要的数据,大小一般可达256KB,一些功能强大的CPU占用近1MB。某些服务器芯片组(如Intel高端Xeon CPU)具备1-2MB。L1缓存一般又分为指令缓存和数据缓存。指令缓存处理有关CPU必须执行的操做的信息,数据缓存则保留要在其上执行操做的数据。如此,减小了争用Cache所形成的冲突,提升了处理器效能。

L2级缓存比L1慢,但大小更大,一般在256KB到8MB之间,功能强大的CPU每每会超过此大小。L2高速缓存保存下一步可能由CPU访问的数据。大多数CPU中,L1和L2高速缓存位于CPU内核自己,每一个内核都有本身的高速缓存。

L3级高速缓存是最大的高速存储单元,也是最慢的。大小从4MB到50MB以上。现代CPU在CPU裸片上具备用于L3高速缓存的专用空间,且占用了很大一部分空间。

2.2 多处理器缓存结构

计算机早已进入多核时代,软件也运行在多核环境。一个处理器对应一个物理插槽、包含多个核(一个核包含寄存器、L1 Cache、L2 Cache),多核间共享L3 Cache,多处理器间经过QPI总线相连。

L1 和 L2 缓存为CPU单个核心私有的缓存,L3 缓存是同插槽的全部核心都共享的缓存。

L1缓存被分红独立的32K数据缓存和32K指令缓存,L2缓存被设计为L1与共享的L3缓存之间的缓冲。大小为256K,主要做为L1和L3之间的高效内存访问队列,同时包含数据和指令。L3缓存包括了在同一个槽上的全部L1和L2缓存中的数据。这种设计消耗了空间,但可拦截对L1和L2缓存的请求,减轻了各个核心私有的L1和L2缓存的负担。

2.3 Cache Line

Cache存储数据以固定大小为单位,称为Cache Line/Block。给定容量和Cache Line size,则就固定了存储的条目个数。对于X86,Cache Line大小与DDR一次访存能获得的数据大小一致,即64B。旧的ARM架构的Cache Line是32B,所以常常是一次填两个Cache Line。CPU从Cache获取数据的最小单位为字节,Cache从Memory获取的最小单位是Cache Line,Memory从磁盘获取数据一般最小是4K。

Cache分红多个组,每组分红多个Cache Line行,大体以下图:

Linux系统下使用如下命令查看Cache信息,lscpu命令也可。

3. 缓存的存取与一致

下面表格描述了不一样存储介质的存取信息,供参考。

存取速度:寄存器 > cache(L1~L3) > RAM > Flash > 硬盘 > 网络存储

以2.2Ghz频率的CPU为例,每一个时钟周期大概是0.5纳秒。

3.1 读取存储器数据

按CPU层级缓存结构,取数据的顺序是先缓存再主存。固然,如数据来自寄存器,只需直接读取返回便可。

  • 1) 如CPU要读的数据在L1 cache,锁住cache行,读取后解锁、返回
  • 2) 如CPU要读的数据在L2 cache,数据在L2里加锁,将数据复制到L1,再执行读L1
  • 3) 如CPU要读的数据在L3 cache,也同样,只不过先由L3复制到L2,再从L2复制到L1,最后从L1到CPU
  • 4) 如CPU需读取内存,则首先通知内存控制器占用总线带宽,后内存加锁、发起读请求、等待回应,回应数据保存至L3,L2,L1,再从L1到CPU后解除总线锁定。

3.2 缓存命中与延迟

因为数据的局部性原理,CPU每每须要在短期内重复屡次读取数据,内存的运行频率远跟不上CPU的处理速度,缓存的重要性被凸显。CPU可避开内存在缓存里读取到想要的数据,称之为命中。L1的运行速度很快,但容量很小,在L1里命中的几率大概在80%左右,L二、L3的机制也相似。这样一来,CPU须要在主存中读取的数据大概为5%-10%,其他命中所有能够在L一、L二、L3中获取,大大减小了系统的响应时间。

高速缓存旨在加快主内存和CPU之间的数据传输。从内存访问数据所需的时间称为延迟,L1具备最低延迟且最接近核心,而L3具备最高的延迟。缓存未命中时,因为CPU需从主存储器中获取数据,致使延迟会更多。

3.3 缓存替换策略

Cache里的数据是Memory中经常使用数据的一个拷贝,存满后再存入一个新的条目时,就须要把一个旧的条目从缓存中拿掉,这个过程称为evict。缓存管理单元经过必定的算法决定哪些数据须要从Cache里移出去,称为替换策略。最简单的策略为LRU,在CPU设计的过程当中,一般会对替换策略进行改进,每一款芯片几乎都使用了不一样的替换策略。

3.4 MESI缓存一致性

在多CPU的系统中,每一个CPU都有本身的本地Cache。所以,同一个地址的数据,有可能在多个CPU的本地 Cache 里存在多份拷贝。为了保证程序执行的正确性,就必须保证同一个变量,每一个CPU看到的值都是同样的。也就是说,必需要保证每一个CPU的本地Cache中可以如实反映内存中的真实数据。

假设一个变量在CPU0和CPU1的本地Cache中都有一份拷贝,当CPU0修改了这个变量时,就必须以某种方式通知CPU1,以便CPU1可以及时更新本身本地Cache中的拷贝,这样才能在两个CPU之间保持数据的同步,CPU之间的这种同步有较大开销。

为保证缓存一致,现代CPU实现了很是复杂的多核、多级缓存一致性协议MESI, MESI具体的操做上会针对单个缓存行进行加锁。

MESI:Modified Exclusive Shared or Invalid

1) 协议中的状态

CPU中每一个缓存行使用4种状态进行标记(使用额外的两位bit表示)

M: Modified

该缓存行只被缓存在该CPU的缓存中,且被修改过(dirty),即与主存中的数据不一致,该缓存行中的内容需在将来的某个时间点(容许其它CPU读取主存中相应内存以前)写回主存。当被写回主存以后,该缓存行的状态变成独享(exclusive)状态

E: Exclusive

该缓存行只被缓存在该CPU的缓存中,未被修改,与主存中数据一致。在任什么时候刻当有其它CPU读取该内存时变成shared状态。一样,当修改该缓存行中内容时,该状态能够变成Modified状态.

S: Shared

意味该缓存行可能被多个CPU缓存,各个缓存中的数据与主存数据一致,当有一个CPU修改该缓存行中,其它CPU中该缓存行能够被做废(Invalid).

I: Invalid,缓存无效(可能其它CPU修改了该缓存行)

2) 状态切换关系

由下图可看出cache是如何保证它的数据一致性的。

譬如,当前核心要读取的数据块在其核心的cache状态为Invalid,在其余核心上存在且状态为Modified的状况。能够从当前核心和其它核心两个角度观察,其它核心角度:当前状态为Modified,其它核心想读这个数据块(图中Modified到Shared的绿色虚线):先把改变后的数据写入到内存中(先于其它核心的读),并更新该cache状态为Share.当前核心角度:当前状态为Invalid,想读这个数据块(图中Invalid到Shared的绿色实线):这种状况下会从内存中从新加载,并更新该cache状态Share

如下表格从这两个角度列举了全部状况,供参考:

3) 缓存的操做描述

一个典型系统中会有几个缓存(每一个核心都有)共享主存总线,每一个相应的CPU会发出读写请求,而缓存的目的是为了减小CPU读写共享主存的次数。

  • 一个缓存除在Invalid状态外均可以知足CPU的读请求,一个Invalid的缓存行必须从主存中读取(变成S或 E状态)来知足该CPU的读请求。
  • 一个写请求只有在该缓存行是M或E状态时才能被执行,若是缓存行处于S状态,必须先将其它缓存中该缓存行变成Invalid(不容许不一样CPU同时修改同一缓存行,即便修改该缓存行中不一样位置的数据也不可),该操做常以广播方式来完成。
  • 缓存能够随时将一个非M状态的缓存行做废,或变成Invalid,而一个M状态的缓存行必须先被写回主存。一个处于M状态的缓存行必须时刻监听全部试图读该缓存行相对主存的操做,操做必须在缓存将该缓存行写回主存并将状态变成S状态以前被延迟执行。
  • 一个处于S状态的缓存行需监听其它缓存使该缓存行无效或独享该缓存行的请求,并将该缓存行变成无效。
  • 一个处于E状态的缓存行也必须监听其它读主存中该缓存行的操做,一旦有这种操做,该缓存行需变成S状态。
  • 对于M和E状态而言老是精确的,和该缓存行的真正状态是一致的。而S状态多是非一致的,若是一个缓存将处于S状态的缓存行做废了,而另外一个缓存实际上可能已经独享了该缓存行,可是该缓存却不会将该缓存行升迁为E状态,是由于其它缓存不会广播做废掉该缓存行的通知,一样,因为缓存并无保存该缓存行的copy的数量,所以也没有办法肯定本身是否已经独享了该缓存行。

从上面的意义来看,E状态是一种投机性的优化:若是一个CPU想修改一个处于S状态的缓存行,总线事务须要将全部该缓存行的copy变成Invalid状态,而修改E状态的缓存不须要使用总线事务。

4. 代码设计的考量

理解计算机存储器层次结构对应用程序的性能影响。若是须要的程序在CPU寄存器中,指令执行时1个周期内就能访问到;若是在CPU Cache中,需1~30个周期;若是在主存中,须要50~200个周期;在磁盘上,大概须要万级周期。另外,Cache Line的存取也是代码设计者须要关注的部分, 以规避伪共享的执行场景。所以,充分利用缓存的结构和机制可有效提升程序的执行性能。

4.1 局部性特性

一旦CPU要从内存或磁盘中访问数据就会产生一个很大的时延,程序性能显著下降,为此咱们不得不提升Cache命中率,也就是充分发挥局部性原理。通常来讲,具备良好局部性的程序会比局部性较差的程序运行得更快,程序性能更好。

局部性机制确保在访问存储设备时,存取数据或指令都趋于汇集在一片连续的区域。一个设计优良的计算机程序一般具备很好的局部性,时间局部性和空间局部性。

1) 时间局部性

若是一个数据/信息项被访问过一次,那么颇有可能它会在很短的时间内再次被访问。好比循环、递归、方法的反复调用等。

2) 空间局部性

一个Cache Line有64字节块,能够充分利用一次加载64字节的空间,把程序后续会访问的数据,一次性所有加载进来,从而提升Cache Line命中率(而非从新去寻址读取)。若是一个数据被访问,那么颇有可能位于这个数据附近的其它数据也会很快被访问到。好比顺序执行的代码、连续建立的多个对象、数组等。数组就是一种把局部性原理利用到极致的数据结构。

3) 代码示例

示例1,(C语言)

//程序 array1.c  多维数组交换行列访问顺序
char array[10240][10240];
 
int main(int argc, char *argv[]){
int i = 0;
int j = 0;
for(i=0; i < 10240 ; i++) {
for(j=0; j < 10240 ; j++) {
  array[i][j] = ‘A’; //按行进行访问
}
}
return 0;
}
//程序array2.c

红色字体的代码调整为: array[j][i] = ‘A’; //按列进行访问

编译、运行结果以下:

从测试结果看,第一个程序运行耗时0.265秒,第二个1.998秒,是第一个程序的7.5倍。

案例参考:https://www.cnblogs.com/wanghuaijun/p/12904159.html

结果分析

数组元素存储在地址连续的内存中,多维数组在内存中是按行进行存储。第一个程序按行访问某个元素时,该元素附近的一个Cache Line大小的元素都会被加载到Cache中,这样一来,在访问紧挨着的下一个元素时,就可直接访问Cache中的数据,不需再从内存中加载。也就是说,对数组按行进行访问时,具备更好的空间局部性,Cache命中率更高

第二个程序按列访问某个元素,虽然该元素附近的一个Cache Line大小的元素也会被加载进Cache中,但接下来要访问的数据却不是紧挨着的那个元素,所以颇有可能会再次产生Cache miss,而不得不从内存中加载数据。并且,虽然Cache中会尽可能保存最近访问过的数据,但Cache大小有限,当Cache被占满时,就不得不把一些数据给替换掉。这也是空间局部性差的程序更容易产生Cache miss的重要缘由之一。

示例2,(Java)

如下代码中长度为16的row和column数组,在Cache Line 64字节数据块上内存地址是连续的,能被一次加载到Cache Line中,在访问数组时命中率高,性能发挥到极致。

public int run(int[] row, int[] column) {
    int sum = 0;
    for(int i = 0; i < 16; i++ ) {
        sum += row[i] * column[i];
    }
    return sum;
}

变量i体现了时间局部性,做为计数器被频繁操做,一直存放在寄存器中,每次从寄存器访问,而不是从缓存或主存访问。

4.2 缓存行的锁竞争

在多处理器下,为保证各个处理器的缓存一致,会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行置成无效状态,当处理器要对这个数据进行修改操做的时候,会强制从新从系统内存里把数据读处处理器缓存里。

当多个线程对同一个缓存行访问时,如其中一个线程锁住缓存行,而后操做,这时其它线程则没法操做该缓存行。这种状况下,咱们在进行程序代码设计时是要尽可能避免的。

4.3 伪共享的规避

连续紧凑的内存分配带来高性能,但并不表明它一直都行之有效,伪共享就是无声的性能杀手。所谓伪共享(False Sharing),是因为运行在不一样CPU上的不一样线程,同时修改处在同一个Cache Line上的数据引发。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素,通常来讲,从代码中很难看清是否会出现伪共享。

在每一个CPU来看,各自修改的是不一样的变量,但因为这些变量在内存中彼此紧挨着,所以它们处于同一个Cache Line上。当一个CPU修改这个Cache Line以后,为了保证数据的一致性,必然致使另外一个CPU的本地Cache的无效,于是触发Cache miss,而后从内存中从新加载变量被修改后的值。多个线程频繁的修改处于同一个Cache Line的数据,会致使大量的Cache miss,于是形成程序性能的大幅降低。

下图说明了两个不一样Core的线程更新同一缓存行的不一样信息项:

上图说明了伪共享的问题。在Core1上运行的线程准备更新变量X,同时Core2上的线程准备更新变量Y。然而,这两个变量在同一个缓存行中。每一个线程都要去竞争缓存行的全部权来更新变量。若是Core1得到了全部权,缓存子系统将会使Core2中对应的缓存行失效。当Core2得到了全部权而后执行更新操做,Core1就要使本身对应的缓存行失效。来来回回的通过L3缓存,大大影响了性能。若是互相竞争的Core位于不一样的插槽,就要额外横跨插槽链接,问题可能更加严重。

1) 规避处理方式

  • 增大数组元素的间隔使得不一样线程存取的元素位于不一样cache line,空间换时间
  • 在每一个线程中建立全局数组各个元素的本地拷贝,而后结束后再写回全局数组

2) 代码示例说明

示例3,(JAVA)

从代码设计角度,要考虑清楚类结构中哪些变量是不变,哪些是常常变化,哪些变化是彻底相互独立,哪些属性一块儿变化。假如业务场景中,下面的对象知足几个特色

public class Data{
    long modifyTime;
    boolean flag;
    long createTime;
    char key;
    int value;
}
  • 当value变量改变时,modifyTime确定会改变
  • createTime变量和key变量在建立后就不会再变化
  • flag也常常会变化,不过与modifyTime和value变量毫无关联

当上面的对象须要由多个线程同时访问时,从Cache角度,当咱们没有加任何措施时,Data对象全部的变量极有可能被加载在L1缓存的一行Cache Line中。在高并发访问下,会出现这种问题:

如上图所示,每次value变动时,根据MESI协议,对象其余CPU上相关的Cache Line所有被设置为失效。其余的处理器想要访问未变化的数据(key和createTime)时,必须从内存中从新拉取数据,增大了数据访问的开销。

有效的Padding方式

正确方式是将该对象属性分组,将一块儿变化的放在一组,与其余无关的放一组,将不变的放到一组。这样当每次对象变化时,不会带动全部的属性从新加载缓存,提高了读取效率。在JDK1.8前,通常在属性间增长长整型变量来分隔每一组属性。被操做的每一组属性占的字节数加上先后填充属性所占的字节数,不小于一个cache line的字节数就可达到要求。

public class DataPadding{
    long a1,a2,a3,a4,a5,a6,a7,a8;//防止与前一个对象产生伪共享
    int value;
    long modifyTime;
    long b1,b2,b3,b4,b5,b6,b7,b8;//防止不相关变量伪共享;
    boolean flag;
    long c1,c2,c3,c4,c5,c6,c7,c8;//
    long createTime;
    char key;
    long d1,d2,d3,d4,d5,d6,d7,d8;//防止与下一个对象产生伪共享
}

采起上述措施后的图示:

在Java中

Java8实现字节填充避免伪共享, JVM参数 -XX:-RestrictContended

@Contended 位于sun.misc用于注解java 属性字段,自动填充字节,防止伪共享。

示例4,(C语言)

//程序 thread1.c   多线程访问数据结构的不一样字段
#include <stdio.h>
#include <pthread.h>
 
struct {
   int a;
   // char padding[64]; // thread2.c代码
   int b;
}data;
 
void *thread_1(void) {
    int i = 0;
for(i=0; i < 1000000000; i++){
    data.a = 0;
}
}
void *thread_2(void) {
    int i = 0;
for(i=0; i < 1000000000; i++){
    data.b = 0;
}
}
 
//程序 thread2.c
Thread1.c 中的红色字体行,打开注释即为 thread2.c
main()函数很简单,建立两个线程并运行,参考代码以下:
pthread_t id1;
int ret = pthread_create(&id1,NULL, (void*)thread_1, NULL);

编译、运行结果以下:

从测试结果看,第一个程序消耗的时间是第二个程序的3倍

案例参考:https://www.cnblogs.com/wanghuaijun/p/12904159.html

结果分析

此示例涉及到Cache Line的伪共享问题。两个程序惟一的区别是,第二个程序中字段a和b中间有一个大小为64个字节的字符数组。第一个程序中,字段a和字段b处于同一个Cache Line上,当两个线程同时修改这两个字段时,会触发Cache Line伪共享问题,形成大量的Cache miss,进而致使程序性能降低。

第二个程序中,字段a和b中间加了一个64字节的数组,这样就保证了这两个字段处在不一样的Cache Line上。如此一来,两个线程即使同时修改这两个字段,两个cache line也互不影响,cache命中率很高,程序性能会大幅提高。

示例5,(C语言)

在设计数据结构的时候, 尽可能将只读数据与读写数据分开,并具尽可能将同一时间访问的数据组合在一块儿。这样CPU能一次将须要的数据读入。譬如,下面的数据结构就很很差。

struct __a
{
   int id; // 不易变
   int factor;// 易变
   char name[64];// 不易变
   int value;// 易变
};
在 X86 下,能够试着修改和调整它
#define CACHE_LINE_SIZE 64  //缓存行长度
struct __a
{
   int id; // 不易变
   char name[64];// 不易变
  char __align[CACHE_LINE_SIZE – sizeof(int)+sizeof(name) * sizeof(name[0]) % CACHE_LINE_SIZE]
   int factor;// 易变
   int value;// 易变
   char __align2[CACHE_LINE_SIZE –2* sizeof(int)%CACHE_LINE_SIZE ]
};

CACHE_LINE_SIZE–sizeof(int)+sizeof(name)*sizeof(name[0])%CACHE_LINE_SIZE看起来不和谐,CACHE_LINE_SIZE表示高速缓存行(64B大小)。__align用于显式对齐,这种方式使得结构体字节对齐的大小为缓存行的大小。

4.4 缓存与内存对齐

1)字节对齐

__attribute__ ((packed))告诉编译器取消结构在编译过程当中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法;

__attribute__((aligned(n)))表示所定义的变量为n字节对齐;

struct B{ char b;int a;short c;}; (默认4字节对齐)

这时候一样是总共7个字节的变量,可是sizeof(struct B)的值倒是12。

字节对齐的细节和编译器实现相关,但通常而言,知足三个准则:

  1. (结构体)变量的首地址可以被其(最宽)基本类型成员的大小所整除;
  2. 结构体每一个成员相对于首地址的偏移量都是成员大小的数倍,若有须要,编译器会在成员之间加上填充字节(internal adding)
  3. 结构体的总大小为结构体最宽基本类型成员大小的数倍,若有须要,编译器会在最末一个成员以后加上填充字节(trailing padding)

2)缓存行对齐

数据跨越两个cache line,意味着两次load或两次store。若是数据结构是cache line对齐的,就有可能减小一次读写。数据结构的首地址cache line对齐,意味着可能有内存浪费(特别是数组这样连续分配的数据结构),因此须要在空间和时间两方面权衡。好比如今通常的malloc()函数,返回的内存地址会已是8字节对齐的,这就是为了可以让大部分程序有更好的性能。

在C语言中,为了不伪共享,编译器会自动将结构体,字节补全和对齐,对齐的大小最好是缓存行的长度。总的来讲,结构体实例会和它的最宽成员同样对齐。编译器这样作是由于这是保证全部成员自对齐以得到快速存取的最容易方法。

__attribute__((aligned(cache_line)))对齐实现
struct syn_str { ints_variable; };__attribute__((aligned(cache_line)));

示例6,(C语言)

struct syn_str { int s_variable; };
void *p = malloc ( sizeof (struct syn_str) + cache_line );
syn_str *align_p=(syn_str*)((((int)p)+(cache_line-1))&~(cache_line-1);

4.5 CPU分支预测

代码在内存里面是顺序排列的,能够顺序访问,有效提升缓存命中。对于分支程序来讲,若是分支语句以后的代码有更大的执行概率,就能够减小跳转,通常CPU都有指令预取功能,这样能够提升指令预取命中的概率。分支预测用的就是likely/unlikely这样的宏,通常须要编译器的支持,属静态的分支预测。如今也有不少CPU支持在内部保存执行过的分支指令的结果(分支指令cache),因此静态的分支预测就没有太多的意义。

示例7,(C语言)

int testfun(int x)
{
   if(__builtin_expect(x, 0)) {
    ^^^--- We instruct the compiler, "else" block is more probable
    x = 5;
    x = x * x;
   } else {
    x = 6;
   }
   return x;
}
在这个例子中,x为0的可能性更大,编译后观察汇编指令,结果以下:
Disassembly of section .text:
00000000 <testfun>:
   0:   55             push   %ebp
   1:   89 e5            mov    %esp,%ebp
   3:   8b 45 08          mov    0x8(%ebp),%eax
   6:   85 c0            test   %eax,%eax
   8:   75 07            jne    11 <testfun+0x11>
   a:   b8 06 00 00 00       mov    $0x6,%eax
   f:   c9             leave
  10:   c3             ret
  11:   b8 19 00 00 00       mov    $0x19,%eax
  16:   eb f7            jmp    f <testfun+0xf>

能够看到,编译器使用的是 jne指令,且else block中的代码紧跟在后面

8:   75 07              jne    11 <testfun+0x11>
a:   b8 06 00 00 00          mov    $0x6,%eax

4.6 命中率的监控

程序设计要追求更好的利用CPU缓存,来减小从内存读取数据的低效。在程序运行时,一般须要关注缓存命中率这个指标。

监控方法(Linux):查询CPU缓存无命中次数及缓存请求次数,计算缓存命中率

perf stat -e cache-references -e cache-misses

4.7 小 结

程序从内存获取数据时,每次不只取回所需数据,还会根据缓存行的大小加载一段连续的内存数据到缓存中,程序设计中的优化范式参考以下。

  • 在集合遍历的场景,可以使用有序数组,数组在内存中是一段连续的空间;
  • 字段尽可能定义为占用字节小的类型,如int可知足时,不使用long。这样单次可加载更多的数据到缓存中;
  • 对象或结构体大小尽量设置为CPU核心缓存行的整数倍。基于64B大小的缓存行,如读取的数据是50B,最坏状况下须要两次从内存加载;当为70B时,最坏状况须要三次内存读取,才能加载到缓存中;
  • 对同一对象/结构体的多个属性,可能存在于同一缓存行中,致使伪共享问题,需为属性的不变与常变,变化的独立与关联而隔离设计,及缓存行对齐,解决多线程高并发环境下缓存失效、彼此牵制问题;
  • CPU有分支预测能力,在使用ifelse case when等循环判断的场景时,能够顺序访问,有效提升缓存的命中

. . . . . .

除了本章节中介绍的案例以外,在系统中CPU Cache对程序性能的影响随处可见。

5. 最后

CPU高速缓存能够说是CPU与内存之间的临时数据交换器,用以有效解决CPU运行处理速度与内存读写速度不匹配的矛盾。缓存设计也一直在发展,尤为是随着内存变得更便宜、更快和更密集。减小内存的延迟一定能够减小现代计算机的瓶颈。

CPU的高速缓存设计,多处理器、多核的多级缓存,带来高效处理的同时也引入了缓存数据的存取与一致性问题,现代CPU实现了很是复杂的多核、多级缓存一致性协议MESI,以保证多个CPU高速缓存之间共享数据的一致。

CPU高速缓存每每须要重复处理相同的数据、执行相同的指令,若是这部分数据、指令都能在高速缓存中找到,便可减小整机的响应时间。程序的设计须要确保充分利用高速缓存的结构与机制,须要利用处理的局部特性,规避锁竞争、伪共享、缓存的失效、彼此的牵制,权衡空间与时间,获取程序运行时的极致与高性能。

. . . . . .

参考博文

 

【系统性能专题一】CPU消耗及问题定位那点事

【系统性能专题二】性能测试及指标分析这点事

【系统性能专题三】CPU高速缓存与极性代码设计

 

点击关注,第一时间了解华为云新鲜技术~

相关文章
相关标签/搜索