cache行line被lock住了还能刷出去吗



要搞清楚 cache行 Line 伪共享的概念及其性能影响需要对现代理器架构和硬件实现有一个基本的了解。
如果读者已经对这些概念已经有所了解可以跳过本小节,直接了解 perf c2c 发现 cache行 Line 偽共享的方法
(注:本节中的所有图片,均来自与 Google 图片搜索版权归原作者所有。)

2.1 存储器层次结构

众所周知现代计算机体系结构,通過存储器层次结构 (Memory Hierarchy) 的设计使系统在性能,成本和制造工艺之间作出取舍从而达到一个平衡。
下图给出了不同层次的硬件访问延迟可鉯看到,各个层次硬件访问延迟存在数量级上的差异越高的性能,往往意味着更高的成本和更小的容量:

通过上图可以对各级存储器 cache荇 Miss 带来的性能惩罚有个大致的概念。

随着多核架构的普及对称多处理器 (SMP) 系统成为主流。例如一个物理 CPU 可以存在多个物理 Core,而每个 Core 又可鉯存在多个硬件线程

而从 OS 内核角度,每个 HT 都是一个逻辑 CPU因此,这个处理器在 OS 来看就是一个 8 个 CPU 的 SMP 系统。

以 x86 为例早期的 x86 就是典型的 UMA 架構。例如下图四路处理器通过 FSB (前端系统总线) 和主板上的内存控制器芯片 (MCH) 相连,DRAM 是以 UMA 方式组织的延迟并无访问差异,

然而这种架构带來了严重的内存总线的性能瓶颈,影响了 x86 在多路服务器上的可扩展性和性能

因此,从 Nehalem 架构开始x86 开始转向 NUMA 架构,内存控制器芯片被集成箌处理器内部多个处理器通过 QPI 链路相连,从此 DRAM 有了远近之分
而 Sandybridge 架构则更近一步,将片外的 IOH 芯片也集成到了处理器内部至此,内存控淛器和 PCIe Root Complex 全部在处理器内部了
下图就是一个典型的 x86 的 NUMA 架构:

由于 NUMA 架构的引入,以下主要部件产生了因物理链路的远近带来的延迟差异:

  • 除粅理 CPU 有本地的 cache行 的层级结构以外还存在跨越系统总线 (QPI) 的远程 cache行 命中访问的情况。需要注意的是远程的 cache行 命中,对发起 cache行 访问的 CPU 来说還是被记入了 LLC cache行 Miss。

  • 在两路及以上的服务器远程 DRAM 的访问延迟,远远高于本地 DRAM 的访问延迟有些系统可以达到 2 倍的差异。
    需要注意的是即使服务器 BIOS 里关闭了 NUMA 特性,也只是对 OS 内核屏蔽了这个特性这种延迟差异还是存在的。

  • 对 CPU 访问设备内存及设备发起 DMA 内存的读写活动而言,存在本地 Device 和远程 Device 的差别有显著的延迟访问差异。

因此对以上 NUMA 系统,一个 NUMA 节点通常可以被认为是一个物理 CPU 加上它本地的 DRAM 和 Device 组成那么,㈣路服务器就拥有四个 NUMA 节点

这个 cache行 条目里既包含了拷贝的内存数据,即 cache行 Line又包含了这行数据在内存里的位置等元数据信息。

由于 cache行 容量远远小于主存因此,存在多个主存地址可以被映射到同一个 cache行 条目的情况下图是一个 cache行 和主存映射的概念图:

而这种 cache行 到主存的映射,通常是由内存的虚拟或者物理地址的某几位决定的取决于 cache行 硬件设计是虚拟地址索引,还是物理地址索引
然而,由于索引位一般設计为低地址位通常在物理页的页内偏移以内,因此不论是内存虚拟或者物理地址,都可以拿来判断两个内存地址是否在同一个 cache行 Line 裏。

下图很好的说明了 cache行 在 CPU 里的真正的组织结构

一个主存的物理或者虚拟地址,可以被分成三部分:高地址位当作 cache行 的 Tag用来比较选中哆路 (Way) cache行 中的某一路 (Way),而低地址位可以做 Index用来选中某一个 cache行 Set。
在某些架构上最低的地址位,Block Offset 可以选中在某个 cache行 Line 中的某一部份

因此,cache行 Line 嘚命中完全依靠地址里的 Tag 和 Index 就可以做到。关于 cache行 结构里的 WaySet,Tag 的概念请参考相关文档或者资料。这里就不再赘述

如前所述,在 SMP 系统裏每个 CPU 都有自己本地的 cache行。因此同一个变量,或者同一行 cache行 Line有在多个处理器的本地 cache行 里存在多份拷贝的可能性,因此就存在数据一致性问题

本文中的 cache行 Line 伪共享场景,就基于上述场景来讲解关于 cache行 一致性协议更多的细节,请参考相关文档

下图即为两个线程间的 cache行 Line 偽共享问题的示意图,

当应用在 NUMA 环境中运行或者应用是多线程的,又或者是多进程间有共享内存满足其中任意一条,那么这个应用就鈳能因为 cache行 Line 伪共享而性能下降

但是,要怎样才能知道一个应用是不是受伪共享所害呢Joe Mario 提交的 patch 能够解决这个问题。Joe 的 patch 是在 Linux 的著名的 perf 工具仩添加了一些新特性,叫做 c2c

  • 这些读者和写者分别的 pid, tid, 指令地址,函数名二进制文件
  • 每个读者和写者的源代码文件,代码行号

最后还囿一个小程序的源代码,可以产生大量的 cache行 Line 伪共享用以测试体验: 

下面,让我们就之前给出的 perf c2c 的输出样例做一个详细介绍。

而 Remote HITM意思是跨 NUMA 节点的 HITM,这个是所有 load 操作里代价最高的情况尤其在读者和写者非常多的情况下,这个代价会变得非常的高

要检查 cache行 Line 伪共享问题,就茬这个表里找 Rmt LLC Load HITM(即跨 NUMA 节点缓存里取到数据的)次数比较高的如果有,就得深挖一下

这是最重要的一个表。为了精简这里只展示了三條 cache行 Line 相关的记录,表格里包含了这些信息:

  • 其中 71,72 行是列名每列都解释了cache行 Line的一些活动。
    • 然后是数据地址列上面提到了 76 行显示了 cache行 Line 的虚擬地址,而下面几行的这一列则是行内偏移
    • 下一列显示了pid,或线程id(如果设置了要输出tid)
    • 接下来三列,展示了平均load操作的延迟我常看着里有没有很高的平均延迟。这个平均延迟可以反映该行的竞争紧张程度。
    • cpu cnt列展示了该行访问的样本采集自多少个cpu
    • 然后是函数名,②进制文件名源代码名,和代码行数
    • 最后一列展示了对于每个节点,样本分别来自于哪些cpu

熟悉 perf 的读者可能已经注意到这里的 -F 选项指萣了非常高的采样频率: 60000。请特别注意:这个采样频率不建议在线上或者生产环境使用因为这会在高负载机器上带来不可预知的影响。

对采样数据的分析可以使用带图形界面的 tui 来看输出,或者只输出到标准输出

默认情况为了规范输出格式,符号名被截断为定长但可以鼡 “--full-symbols” 参数来显示完整符号名。

有的时候很需要找到读写这些 cache行 Line 的调用者是谁。下面是获得调用图信息的方法但一开始,一般不会一仩来就用这个因为输出太多,难以定位伪共享一般都是先找到问题,再回过头来使用调用图

3.4 如何增加采样频率

提升采样频率,可以短时间获得更丰富更可靠的采样集合。想提升采样频率可以用下面的方法。

3.5 如何让避免采样数据过量

在大型系统上(比如有 4,8,16 个物理 CPU 插槽的系统)运行 perf c2c可能会样本太多,消耗大量的CPU时间perf.data文件也可能明显变大。 对于这个问题有以下建议(包含但不仅限于):

一般搭建看见性能工具的输出,都会问这些数据意味着什么Joe 总结了他使用 c2c 优化应用时,学到的东西

  • 有的时候,一段代码它不在某一行 cache行 Line 上竞爭严重,但是它却在很多 cache行 Line 上竞争这样的代码段也是很值得优化的。同理还有多进程程序访问共享内存时的情况
  • 在 Pareto 表里,如果发现很長的 load 操作平均延迟常常就表明存在严重的伪共享,影响了性能
  • 接下来去看样本采样自哪些节点和 CPU,据此进行优化将哪些内存或 Task 进行 NUMA 節点锁存。

最后Pareto 表还能对怎么解决对齐得很不好的cache行 Line,提供灵感 例如:

  • 很容易定位到:写地很频繁的变量,这些变量应该在自己独立嘚 cache行 Line可以据此进行对齐调整,让他们不那么竞争运行更快,也能让其它的共享了该 cache行 Line 的变量不被拖慢
  • 很容易定位到:读多写少的变量,可以将这些变量组合到相同或相邻的 cache行 Line

3.7 使用原始的采样数据

最后,在文章末尾Joe 给出了如下总结,并在博客中致谢了所有的贡献者:

Linux perf c2c 功能在上游的 4.2 内核已经可用了这是集体努力的结果。

首先引入一段代码指出Java内存模型存在的问题:启动两个线程t1t2访问共享变量sharedVariablet2线程逐渐将sharedVariable自增到MAX每自增一次就休眠500ms放弃CPU执行权,期望此间另外一个线程t1能够在第7-12行轮询過程中发现到sharedVariable的改变并将其打印

CAS操作在x86上是由cmpxchg(Compare Exchange)实现的(不同指令集有所不同)而Java中并未公开CAS接口,CAS以``compareAndSetXxx的形式定义在Unsafe类(仅供Java核心类庫调用)中我们可以通过反射调用,但是JDK提供的AtomicXxx`系列原子操作类已能满足我们的大多数需求

于是我们来看一下启动十个线程执行次i++在使用CAS和使用synchronized两种情况下的性能之差:

但是我们的疑问还没解开,为什么原子类的CAS更新具有volatile写的语义单单CAS只能确保use -> assgin是原子的啊。

看一下原孓类的源码就知道了以AtomicInteger,其他的都类同:

你会发现原子类封装了一个volatile域豁然开朗吧。CAS更新的volatile域我们知道volatile域的更新将会导致两件事发苼:

  • 通知其他CPU将缓存行置为无效

volatile的另一个语义就是禁止指令重排序,即volatile产生的汇编指令lock具有个指令屏障使得该屏障之前的指令不能重排序箌屏障之后这个作用使用单例模式的并发优化案例来说再好不过了。

利用类加载过程的初始化(当类被主动引用时应当立即对其初始化)阶段会执行类构造器<clinit>按照显式声明为静态变量初始化的特点(类的主动引用、被动引用、类构造器、类加载过程详见《深入理解Java虚拟機(第二版)》)

什么是对类的主动引用:

  • newgetStaticputStaticinvokeStatic四个字节码指令涉及到的类,对应语言层面就是创建该类实例、读取该类静态字段、修妀该类静态字段、调用该类的静态方法
  • 当初始化一个类时如果他的父类没被初始化,那么先初始化其父类
  • 当JVM启动时首先会初始化main函数所在的类

什么是对类的被动引用:

  • 通过子类访问父类静态变量,子类不会被立即初始化
  • 通过数组定义引用的类不会被立即初始化
  • 访问某个類的常量该类不会被立即初始化(因为经过编译阶段的常量传播优化,该常量已被复制一份到当前类的常量池中了)

需要的时候才去创建实例(这样就能避免暂时不用的大内存对象被提前加载):

上例中的饿汉模式在单线程下是没问题的但是一旦并发调用getInstance,可能会出现t1線程刚执行完第6行还没来得及创建对象t2线程就执行到第6行的判断了,这会导致多个线程来到第7行并执行导致SingletonObject2被实例化多次,于是我们將第6-7行通过synchronized串行化:

我们已经知道synchronized是重量级锁如果单例被实例化后,每次获取实例还需要获取锁长期以往,开销不菲因此我们在获取实例时加上一个判断,如果单例已被实例化则跳过获取锁的操作(仅在初始化单例时才可能发生冲突):

这样真的就OK了吗确实同一时刻只有一个线程能够进入到第9行创建对象,但是你别忘了new Object()是可以被分解的!其对应的伪指令如下:

而且上述三步是没有依赖关系的这意菋着他们可能被重排序成下面的样子:

这时可能会导致t1线程执行到第2行时,t1线程判断instance引用地址不为null于是去使用这个instance而这时对象还没构造唍!!这意味着如果对象可能包含的引用变量为null而没被正确初始化,如果t1线程刚好访问了该变量那么将抛出空指针异常

我们还可以利用类呮被初始化一次的特点将单例定义在内部类中从而写出更加优雅的方式:

枚举实例的构造器只会被调用一次

这是由JVM规范要求的,JVM实现必須保证的

缓存击穿是指缓存中没有但数据庫中有的数据(一般是缓存时间到期)这时由于并发用户特别多,同时读缓存没读到数据又同时去数据库去取数据,引起数据库压力瞬间增大造成过大压力。

1、设置热点数据永远不过期

2、加互斥锁,互斥锁参考代码如下:

// 取得每個 Key專屬的 lock object;若同時有多個 thread要求相同資料只會(到資料庫)查第一次,剩下的從 cache行讀取
 
 
 //執行取得資料的委派作業
 
 
 

1、缓存中有数据直接走下述代码就返回结果了

2、缓存中没有数据,第1个进入的线程获取锁并从数据库去取数据,没释放锁之前其他并行进入的线程会等待,再重新去缓存取数据这样就防止都去数據库重复取数据,重复往缓存中更新数据情况出现

3、取得每个 Key专有的 lock object;若同时有多个 thread要求相同资料,只会(到数据库)查第一次剩下的从 cache荇读取。 // 取得每個 Key專屬的 lock object;若同時有多個 thread要求相同資料只會(到資料庫)查第一次,剩下的從 cache行讀取

以上就是c# 如何用lock解决缓存击穿的详细内嫆

我要回帖

更多关于 cache行 的文章

 

随机推荐