阅读 112

cpu和cache哪个速度快,异步io效率最高

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

在一致性这个问题域中,每一级自由金鱼大约为:条

一致性模型定义了各种一致性模型的理论基础硬件层,并提供了实现特定一致性模型的硬件能力。 硬件默认以最基本的方式工作。 例如

对同一个线程没有数据依存性的指令可以变更顺序进行最优化执行,有数据依存性的指令按照程序顺序执行。 这样就保证了单线程程序执行的正确性,保证了通过读取操作读取的数据一定是以前写入同一位置的数据3 .语言层,少数语言在语言水平上提供了满足一致性模型的编程能力,其他语言是硬件提供一致语言的机制有以下:种

如果APP应用层需要使用此编程能力作为资源,例如volitile、synchronized和Happens-before规则,则必须明确申请。 例如,您可能需要使用volatile来识别变量为编译器提供一致的编程能力,以适应各种底层硬件平台。 例如,一些平台可能使用内存屏障,而另一些平台可能使用读-修改-写入。 我们需要语言层来屏蔽这种差异化的语言层。 少数语言在语言层级别提供满足一致模型的编程能力,其他语言直接使用硬件层提供一致的编程能力。 提供一致语言的机制有以下:种

如果APP应用层需要使用此编程能力作为资源,例如volitile、synchronized和Happens-before规则,则必须明确申请。 例如,您可能需要使用volatile来识别变量为编译器提供一致的编程能力,以适应各种底层硬件平台。 例如,一些平台使用内存屏障,而一些平台使用读-修改-写。 要阻止这种区分APP应用层,需要语言层。 例如,在并发服务器程序这样的分布式系统中,一致性问题的工作如下

根据实际需求定义APP应用程序必须满足的一致性要求,并选择实现适当一致性要求的算法。 例如,在分布式存储中通过消息协议实现的Paxos、Zab、多级提交等利用编程语言提供基本的一致性编程的能力,是实现一致性要求算法的基础说了一堆一致性需求相关的,那么问题来了,为什么有内存一致性的这个需求呢?

内存一致性的需要主要是由于多核CPU的出现和存在多级缓存。 这将导致读取和写入内存的并发性问题,并导致内存一致性问题。

因此,缓存是引起内存一致性问题的重要原因之一。 在写Java存储器模型的很多报道中,由于在CPU的写入时存在写入缓冲区write buffer,导致了写入不能立即返回主存储器,其他线程无法看到新写入的值的所谓可视性问题而且,由于写缓存是一种lazy write,所以CPU可以在写入操作尚未刷新到存储器时开始后续读取,这也产生了重新排列的情况,即所谓的有序性问题。

本文写了与CPU缓存相关的机制,以了解写缓存是什么。 本人并不研究硬件,有些观点也是基于自己的理解,如果有错误请参考资料。

先来看一张图,这张就是Java内存模型的概念模型图,工作内存 work memory是对CPU寄存器和高速缓存的抽象。

再来看一张图,摘自 《深入理解计算机系统》 中描述Intel Core i7处理器的高速缓存的概念模型。

通过比较这两幅图,可以看到Java内存模型中每个线程的工作内存实际上是寄存器和缓存的抽象。 目前主流的多核处理器设计通常包括每个内核一个L1和L2缓存,多个内核共享一个L3缓存。 各核心直接通过系统总线连接。 系统总线包括数据总线、地址总线、控制总线,总称为系统总线。 请记住,总线是共享资源,如果使用不当,例如存储一致性协议导致的总线流量风暴,将会影响程序的执行效率。

此图像显示了每个类缓存的几个参数,其中有几个点:

http://www.Sina.com/http://www.Sina.com /

i-cache存储命令是只读的,

d-cache存储数据位于http://www.Sina.com/http://www.Sina.com/http://www.com/3358 www.Sina.com/http://ww.com/http://ww.com

CPU只直接和寄存器已经L1缓存交互

到计算机领域的局部性原理(Principle of Locality)。局部性原理是缓存技术的底层理论基础。局部性包括两种形式:


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

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

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

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

另外来看一下存储器的体系结构

有几个要点

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

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

划分为S个缓存组 cache set每组里面有E个缓存行 cache line,也叫Cache线,行数E也叫缓存的相联度每行里面1个有效位来标记该缓存行是否dirty,有t个长度的标记位来辅助缓存地址定位,标识该缓存块的唯一位置,有一个B个字节的缓存块block。一行只有一个块高速缓存的大小C = B * E * S,只计算有效的字节数,不包括有效位及标记位的大小一个高速缓存可以用一个四元组来表示(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(组数)

先看高速缓存是如何在当前缓存中定位一个目标内存地址的缓存并读命中的,分为三步

组选择行匹配字抽取

这个定位的过程有点类似pldmt操作,把一个m位的内存地址映射到一个高速缓存的组索引(s位),行(t位),块偏移(b位)中去。

还拿Core i7的L1缓存(64,8,64,64)来说,拿到一个64位的内存地址

组选择:有64个组,那么64位的内存地址中就要拿出s=6位(000000-111111)来表示64个组号,根据这个内存地址的s位定位到一个组行匹配:每个组有8行,大小为64B的块得到的b=6, 计算得到t = m - (b+s) = 64 - 12 = 52,也就是说64位地址的高52位作为t,用这个t标记去这个组的8个行去匹配对应t标记位,如果有匹配的行,就命中,否则不命中如果命中,再由这个内存地址的低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个状态

读命中读不命中写命中写不命中

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

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

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

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

直写 write-through, 这种方式会写高速缓存和内存写回,也有叫回写的,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


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐