死磕Java多线程(五)---理解CPU高速缓存的工作原理

2023-10-10 08:50

本文主要是介绍死磕Java多线程(五)---理解CPU高速缓存的工作原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我们说了Java内存模型是一个语言级别的内存模型抽象,它屏蔽了底层硬件实现内存一致性需求的差异,提供了对上层的统一的接口来提供保证内存一致性的编程能力。

在一致性这个问题域中,各个层面扮演的角色大致如下:

  1. 一致性模型,定义了各种一致性模型的理论基础
  2. 硬件层,提供了实现某些一致性模型的硬件能力。硬件在默认情况下按照最基本的方式运行,比如
    对同一个线程没有数据依赖的指令可以重排序优化执行,有数据依赖的指令按照程序顺序执行,从而保证单线程程序运行的正确性保证读操作读到的数据肯定是之前在同一位置写入的数据3. 语言层,少数语言提供了语言层面的满足一致性模型的编程能力,另外一些语言则直接使用硬件层提供了一致性编程的能力。提供一致性能力语言的工作方式如下:
    把满足一致性需求的编程能力作为一种资源,指定一些规则,比如volitile, synchronized,Happens-before规则等当应用层需要使用这种编程能力的时候,需要显式地提出申请,比如显式地使用volatile来标识变量通过编译器适配底层各种硬件平台提供了一致性编程的能力,比如有些平台使用内存屏障,有些平台使用read-modified-write,需要语言层来屏蔽这种差异性
  3. 语言层,少数语言提供了语言层面的满足一致性模型的编程能力,另外一些语言则直接使用硬件层提供了一致性编程的能力。提供一致性能力语言的工作方式如下:
    把满足一致性需求的编程能力作为一种资源,指定一些规则,比如volitile, synchronized,Happens-before规则等当应用层需要使用这种编程能力的时候,需要显式地提出申请,比如显式地使用volatile来标识变量通过编译器适配底层各种硬件平台提供了一致性编程的能力,比如有些平台使用内存屏障,有些平台使用read-modified-write,需要语言层来屏蔽这种差异性
  4. 应用层,比如分布式系统,比如并发的服务器程序,它们在一致性问题中的工作有
    根据实际需求来定义应用所需要满足的一致性需求定义和选择相应的实现一致性需求的算法,比如分布式存储中通过消息协议实现的Paxos,Zab,多阶段提交等利用编程语言提供了基本的一致性编程的能力作为实现一致性需求算法的基础

说了一堆一致性需求相关的,那么问题来了,为什么有内存一致性的这个需求呢?

内存一致性需求的出现主要是因为多核CPU的出现,并且存在多级的高速缓存,这样就出现了对内存读写的并发问题,从而出现了内存的一致性问题。

所以高速缓存是造成内存一致性问题的一个重要原因。很多写Java内存模型的文章笼统的说CPU写操作的时候存在一个写缓冲区write buffer,导致写操作不能及时写回到主存,造成了其他线程不能看到新写入的值,也就是所谓的可见性问题; 并且由于写缓存区是一种lazy write,导致了CPU可以在写没有刷新到内存的时候就开始后续的读,也形成了重排序的场景,所谓的有序性的问题。

这篇文章写写CPU高速缓存相关的工作原理,来看看写缓存区到底是个什么东西。本人不是研究硬件的,一些观点也是基于自己的理解,如果说的不对请进一步查阅资料。

先来看一张图,这张就是Java内存模型的概念模型图,工作内存 work memory是对CPU寄存器和高速缓存的抽象。
在这里插入图片描述
再来看一张图,摘自《深入理解计算机系统》中描述Intel Core i7处理器的高速缓存的概念模型。
在这里插入图片描述
对比这两张图,我们可以看到Java内存模型中每个线程的工作内存实际上就是寄存器以及高速缓存的抽象。在目前主流的多核处理器设计中,一般每个核心都会包含1个L1缓存和L2缓存,多个核心共享一个L3高速缓存。各个核心直接通过系统总线连接。系统总线包括数据总线,地址总线,控制总线,统称系统总线。我们要记住的是总线是一种共享的资源,如果不合理的使用,比如存一致性协议导致的总线流量风暴,会影响程序执行的效率。

这张图说了各级高速缓存的一些参数,有几个要点:

  1. CPU只直接和寄存器已经L1缓存交互
  2. 现代的L1缓存分为两个单独的物理块:
    i-cache存储指令,是只读的,
    d-cache存储数据,是读写的
  3. L2和L3缓存存储指令和数据
  4. 注意高速缓存的大小,Core i7的L1缓存大小为64KB, L2缓存是256KB,L3是8MB
  5. 缓存是分块,分组的
  6. L1的访问周期是4, L2是L1的3倍,L3是L2的3倍
  7. 一次内存访问的时钟周期是L3的3倍左右,和L1差2个数量级
  8. 一次硬盘(普通磁盘)访问的时间在1-10ms级别,和一次内存访问差4个数量级,和1次高速缓存访问差6个数量级以上
  9. 一次固态硬盘访问的时间在10-100微秒级别,比普通硬盘快1到2个数量级,和一次内存访问差2-3个数量级左右

在这里插入图片描述
说到高速缓存就不得不说到计算机领域的局部性原理(Principle of Locality)。局部性原理是缓存技术的底层理论基础。局部性包括两种形式:

  1. 时间局部性,一个具有良好时间局部性的程序中,被引用过一次的存储器位置很可能在不远的将来再被多次引用
  2. 空间局部性,一个具有良好空间局部性的程序中,如果一个存储器位置被引用了一次,那么程序很可能在不远的将来引用附近的一个存储器位置

我们知道64位机器一次内存数据读取64位,也就是8个字节,8个连续的内存位置,所以高速缓存中存放的也是连续位置的数据,这是局部性的体现

局部性对编程的一些指导:

  1. 重复引用同一个变量具有良好的时间局部性
  2. 对于具有步长为k的引用模式的程序,步长越短空间局部性越好。尤其是操作数组,多维数组,局部性的影响很大
  3. 对于取指令来说,循环有好的时间和空间局部性,循环体越小,循环次数越多,局部性越好

另外来看一下存储器的体系结构
在这里插入图片描述
有几个要点

  1. 越往上存储容量越小,存取速度越快,成本越高,反之亦然
  2. 一层存储器只和下层存储器打交道,不会跨级访问
  3. 下层作为上层的一个缓存。CPU要访问的数据的最终一般都经过主存,主存作为下层其他设备的一个缓存,其他设备的数据最终都要进入主存才能被CPU访问到。比如磁盘文件读取操作,CPU只发起操作请求,具体的数据操作不需要经过CPU,由DMA(Direct Memory Access)来操作IO和主存的交互,当操作完成后,IO设备发出中断,通知CPU操作完成
  4. 每层缓存都需要一个管理器来管理缓存,比如将缓存划分为块,在不同层中传送块,判定命中不命中。管理器可以是硬件,软件或两者的集合。比如高速缓存完全由内置在缓存中的硬件来管理

下面正式进入高速缓存工作原理的主题,先看一下高速缓存的基本结构

  1. 划分为S个缓存组 cache set
  2. 每组里面有E个缓存行 cache line,也叫Cache线,行数E也叫缓存的相联度
  3. 每行里面1个有效位来标记该缓存行是否dirty,有t个长度的标记位来辅助缓存地址定位,标识该缓存块的唯一位置,有一个B个字节的缓存块block。一行只有一个块
  4. 高速缓存的大小C = B * E * S,只计算有效的字节数,不包括有效位及标记位的大小
  5. 一个高速缓存可以用一个四元组来表示(S, E, B, m),m表示计算机的位数。拿Core i7的L1缓存来说,S = 64, E = 8, B = 64, m = 64,可以表示为(64,8,64,64).

可以看到L1的大小32K = 64个字节(块大小) * 8(行数) * 64(组数)
在这里插入图片描述
先看高速缓存是如何在当前缓存中定位一个目标内存地址的缓存并读命中的,分为三步

  1. 组选择
  2. 行匹配
  3. 字抽取

这个定位的过程有点类似哈希操作,把一个m位的内存地址映射到一个高速缓存的组索引(s位),行(t位),块偏移(b位)中去。
在这里插入图片描述
还拿Core i7的L1缓存(64,8,64,64)来说,拿到一个64位的内存地址

  1. 组选择:有64个组,那么64位的内存地址中就要拿出s=6位(000000-111111)来表示64个组号,根据这个内存地址的s位定位到一个组
  2. 行匹配:每个组有8行,大小为64B的块得到的b=6, 计算得到t = m - (b+s) = 64 - 12 = 52,也就是说64位地址的高52位作为t,用这个t标记去这个组的8个行去匹配对应t标记位,如果有匹配的行,就命中,否则不命中
  3. 如果命中,再由这个内存地址的低b位计算出这个地址在块中的偏移位置。块可以理解为一个字节数组,64个字节的块就有块[0]…块[63]个偏移量,有内存地址的低b位可以计算得到这个地址对应的偏移量,从而找到这个数

比如对于一个32个元素的int数组int[32]来说,int[0] - int[15]存放到高速缓存组[0]的第0行,一个块是64个字节,正好可以存储16个int数据。int[16] - int[31]存放到高速缓存组[0]的第1行。当访问int[0]的时候,没有命中,会从下一层存储器加载0行的缓存块,这样int[0]-int[15]都加载到缓存块中了,下一次访问int[1] - int[15]的时候都命中。访问到Int[16]的时候没有命中,同样从下一层存储中加载int[16] - int[31]到第1行,这样下次访问int[16]

  • int[31]时就都命中

高速缓存有直接映射高速缓存,E路相联高速缓存,全相联高速缓存之分,区别是直接相联高速缓存每一组只有1行,所以只要定位到组就能知道是否命中。全相联高速缓存则相反,只有1组,只要匹配到t位的标记位就知道是否命中。

E路相联高速缓存则是折中,比如Core i7的L1 d-cache就是8路相联高速缓存,每组有8行,这样定位到组之后,还需要在组的8个行里面去匹配标记位来判断是否命中。

缓存的常用术语命中hit表示在当前缓存中定位到了目标地址的缓存,不命中表示在当前缓存中没有找到目标地址的缓存。
结合读写动作,所以有4个状态

  1. 读命中
  2. 读不命中
  3. 写命中
  4. 写不命中

知道了如何把一个内存地址映射到高速缓存块中之后,我们来分析这4种情况各自的表现

读命中
最简单的情况,按照组选择,行匹配,数据抽取的步骤返回命中的数据

读不命中
读不命中的话就需要从下一层存储去加载对应的数据项来对应的缓存行中,注意加载的时候是整个缓存块都会被新的缓存块所代替。替换的时候比较复杂,要判断替换掉哪个缓存行。最常用的作法是使用LRU(least recently used)算法,最近最少使用算法,替换最后一次访问时间最久远的那一行。然后返回加载后找到的数据

关于写,情况就更复杂,这也是常说的CPU lazy write的原因。CPU写高速缓存有两种方式

  1. 直写 write-through, 这种方式会写高速缓存和内存
  2. 写回,也有叫回写的,write-back,这种方式只写高速缓存,将相应的缓存行标记为脏dirty,我们前面说了每个缓存行有一个有效位,0表示dirty/空, 1表示有效。只有当这个脏的缓存行要被替换掉时,才会写到内存中去

在写命中的情况下,由于write-through要写高速缓存和内存,每次写都会造成总线流量。write-back只写高速缓存,不产生总线流量
当写不命中的情况下,有两种方法:写分配 write-allocate 和非写分配 not-write-allocate。写分配会从下一层存储加载相应的块到高速缓存,然后更新这个缓存块。非写分配会直接避开高速缓存,直接写到主存。一般都是write-back使用write-allocate的方式,write-through使用not-write-allocate的方式。

我们比较一下write-through和write-back的特点

write-through: 每次写都会写内存,造成总线流量,性能较差,优点是实时性强,不会因为断电丢失数据

write-back: 充分利用局部性原理,脏的缓存线也能被后面的读立刻读取,性能较高。缺点是实时性不高,出现故障可能会丢失数据

目前基本上CPU的写缓存都采用write-back的方式,不过可以通过BIOS或者操作系统内核参数来配置CPU采取哪种写的方式。

下面这两张来自wiki的图说清了write-through和write-back的流程
在这里插入图片描述

在这里插入图片描述

那么别人经常提到的写缓冲区write-buffer到底是个什么东西呢,write-buffer被write-through时使用,用来缓存写回到主内存的数据,我们知道写一次内存要100ns左右,CPU不会等待写直到写入内存才继续执行后续指令,它是把要写到主存的数据放到write-buffer,然后就执行后面的指令了,可以理解为一种异步的方式,来优化write-through的性能。如果write buffer满了,那么后续的写要等待write buffer中有空位置才能继续写。

理解下缓冲区的概念,缓冲区是用来适配两个流速不同的组件常用的方式,比如IO中的BufferedWriter,生产者-消费者模式的缓冲队列等等,它可以很好地提高系统的性能。

可以看到,不管是write-through,还是write-back,由于高速缓存和写缓冲区的存在,它们都造成了lazy write的现象,写不是马上就写回到主内存,从而造成了数据可见性和有序性的问题,所以需要定义内存模型来提供一些手段来保证一些一致性需求,比如通过使用内存屏障强制把高速缓存/写缓冲区中的数据写回到内存,或者强制把高速缓存中的数据刷新,来保证数据的可见性和有序性。

原文链接:https://blog.csdn.net/ITer_ZC/article/details/41979189

这篇关于死磕Java多线程(五)---理解CPU高速缓存的工作原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/179372

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于