原文地址 作者:Mechanical Sympathy 译者:潘曦 校对:Simon-SZ ,方腾飞
即使是资深的技术人员,我经常听到他们谈论某些操作是如何导致一个CPU缓存的刷新。看来这是关于CPU缓存如何工作和缓存子系统如何与执行核心交互的一个常见误区。本文将致力于解释CPU缓存的功能以及执行程序指令的CPU核心如何与缓存交互。我将以最新的Intel x86 CPU为例进行说明,其他CPU也使用相似技术以达到相同目的。
绝大部分常见的现代系统都被设计成在多处理器上共享内存。共享内存的系统都有一个单独的内存资源,它会被两个或者更多的独立CPU核心同时访问。核心到主存的延迟变化范围很大,大约在10-100纳秒。在100ns内,一个3GH的CPU可以处理多达1200条指令。每一个Sandy Bridge的CPU核心,在每个CPU时钟内可以并行处理4条指令。CPU使用缓存子系统避免了处理核心直接访问主存的延迟,这样能使CPU更高效的处理指令。一些缓存很小、非常快速并且集成在每个核心之内;而另一些则慢一些、更大、在各个核心间共享。这些缓存与寄存器和主内存一起构成了非持久性的内存体系。
当你在设计一个重要算法时要记住,缓存不命中所导致的延迟,可能会使你失去执行500条指令时间!这还仅是在单插槽(single-socket)系统上,如果是多插槽(multi-socket)系统,由于内存访问需要跨槽交互,可能会导致双倍的性能损失。
内存体系
图1.对于2012 Sandy Bridge核心来说,内存模型可以大致按照如下进行分解:
1.寄存器:在每个核心上,有160个用于整数和144个用于浮点的寄存器单元。访问这些寄存器只需要一个时钟周期,这构成了对执行核心来说最快的内存。编译器会将本地变量和函数参数分配到这些寄存器上。当使用超线程技术( hyperthreading )时,这些寄存器可以在超线程协同下共享。
2.内存排序缓冲(Memory Ordering Buffers (MOB) ):MOB由一个64长度的load缓冲和36长度的store缓冲组成。这些缓冲用于记录等待缓存子系统时正在执行的操作。store缓冲是一个完全的相关性队列,可以用于搜索已经存在store操作,这些store操作在等待L1缓存的时候被队列化。在数据与缓存子系统传输时, 缓冲可以让处理器异步运转。当处理器异步读或者异步写的时候,结果可以乱序返回。为了使之与已发布的内存模型( memory model )一致,MOB用于消除load和store的顺序。
3.Level 1 缓存:L1是一个本地核心内的缓存,被分成独立的32K数据缓存和32K指令缓存。访问需要3个时钟周期,并且当指令被核心流水化时, 如果数据已经在L1缓存中的话,访问时间可以忽略。
4.L2缓存:L2缓存是一个本地核心内的缓存,被设计为L1缓存与共享的L3缓存之间的缓冲。L2缓存大小为256K,主要作用是作为L1和L3之间的高效内存访问队列。L2缓存同时包含数据和指令。L2缓存的延迟为12个时钟周期。
5.L3缓存: 在同插槽的所有核心都共享L3缓存。L3缓存被分为数个2MB的段,每一个段都连接到槽上的环形网络。每一个核心也连接到这个环形网络上。地址通过hash的方式映射到段上以达到更大的吞吐量。根据缓存大小,延迟有可能高达38个时钟周期。在环上每增加一个节点将消耗一个额外的时钟周期。缓存大小根据段的数量最大可以达到20MB。L3缓存包括了在同一个槽上的所有L1和L2缓存中的数据。这种设计消耗了空间,但是使L3缓存可以拦截对L1和L2缓存的请求,减轻了各核心私有的L1和L2缓存的负担。
6.主内存:在缓存完全没命中的情况下,DRAM通道到每个槽的延迟平均为65ns。具体延迟多少取决于很多因素,比如,下一次对同一缓存 行中数据的访问将极大降低延迟,而当队列化效果和内存刷新周期冲突时将显著增加延迟。每个槽使用4个内存通道聚合起来增加吞吐量,并通过在独立内存通道上流水线化(pipelining )将隐藏这种延迟。
7. NUMA:在一个多插槽的服务器上,会使用非一致性内存访问( non-uniform memory access )。所谓的非一致性是指,需要访问的内存可能在另一个插槽上,并且通过 QPI 总线访问需要额外花费40ns。 Sandy Bridge对于以往的兼容系统来说,在2插槽系统上是一个巨大的进步。在 Sandy Bridge上,QPI总线的能力从6.4GT/s提升到8.0GT/s,并且可以使用两条线路,消除了以前系统的瓶颈。对于 Nehalem and Westmere 来说,QPI只能使用内存控制器为一个单独插槽分配的带宽中的40%,这使访问远程内存成为一个瓶颈。另外,现在QPI链接可以使用预读取请求,而前一代系统不行。
关联度(Associativity Levels)
缓存是一个依赖于hash表的高效硬件。使用hash函数常常只是将地址中低位bit 进行映射 ,以实现缓存索引。hash表需要有解决对于同一位置冲突的机制。 关联度就是hash表中槽(slot)的数量,也被称为组(ways)和集合(sets),可以用来存储一个内存地址的hash版本。关联度的多少需要在存储数据的容量,耗电量和查询时间之间寻找平衡。(校对注:关联度越高,槽的数量越多,hash冲突越小,查询速度越快)
对于Sandy Bridge,L1和L2是8路组相连 ,L3是12路组相连 。(For Sandy Bridge the L1D and L2 are 8-way associative, the L3 is 12-way associative.)
缓存一致性
由于一些缓存在内核本地,我们需要一些方法保证一致性,使所有核心的内存视图一致。对于主流系统来说,内存子系统需要考虑“真实的来源(source of truth)”。如果数据只从缓存中来,那么它永远不会过期;当数据同时在缓存和主内存中存在时,缓存中存的是主拷贝(master copy)。这种内存管理被称为写-回(write-back),在此方式下,当新的缓存行占用旧行,导致旧行被驱逐时,缓存数据只会被写回主内存中。x86架构的每个缓存块的大小为64 bytes,称为缓存行( cache-line)。其它种类的处理器的缓存行大小可能不同。更大的缓存行容量降低延迟,但是需要更大的带宽(校对注:数据总线带宽)。
为了保证缓存的一致性,缓存控制器跟踪每一个缓存行的状态,这些状态的数量是有限的。Intel使用MESIF协议,AMD使用 MOESI。在MESIF协议下,缓存行处于以下5个状态中的1个。
被修改(Modified):表明缓存行已经过期,在接下来的场景中要写回主内存。当写回主内存后状态将转变为排它( Exclusive )。
独享(Exclusive):表明缓存行被当前核心单独持有,并且与主内存中一致。当被写入时,状态将转变为修改(Modified)。要进入这个状态,需要发送一个 Request-For-Ownership (RFO)消息,这包含一个读操作再加上广播通知其他拷贝失效。
共享(Shared):表明缓存行是一个与主内存一致的拷贝。
失效(Invalid):表明是一个无效的缓存行。
向前( Forward ):一个特殊的共享状态。用来表示在NUMA体系中响应其他缓存的特定缓存。
为了从一个状态转变为另一个状态,在缓存之间,需要发送一系列的消息使状态改变生效。对于上一代(或之前)的 Nehalem核心的Intel CPU和 Opteron核心的AMD CPU,插槽之间确保缓存一致性的流量需要通过内存总线共享,这极大地限制了可扩展性。如今,内存控制器的流量使用一个单独的总线来传输。例如,Intel的QPI和AMD的HyperTransport就用于插槽间的缓存一致性通讯。
缓存控制器作为L3缓存段的一个模块连接到插槽上的环行总线网络。每一个核心,L3缓存段,QPI控制器,内存控制器和集成图形子系统都连接到这个环行总线上。环由四个独立的通道构成,用于:在每个时钟内完成请求、嗅探、确认和传输32-bytes的数据(The ring is made up of 4 independent lanes for: request,snoop, acknowledge, and 32-bytesdata per cycle)。L3缓存包含所有L1和L2缓存中的缓存行,这有助于帮助核心在嗅探变化时快速确认改变的行。用于L3缓存段的缓存控制器记录了哪个核心可能改变自己的缓存行。
如果一个核心想要读取一些数据,并且这些数据在缓存中并不处于共享、独占或者被修改状态;那么它就需要在环形总线上做一个读操作。它要么从主内存中读取(缓存没命中),要么从L3缓存读取(如果没过期或者被其他核心嗅探到改变)。在任何情况下,一致性协议都能保证,读操作永远不会从缓存子系统返回一份过期拷贝。
并发编程
如果我们的缓存总是保证一致性,那么为什么我们在写并发程序时要担心可见性?这是因为核心为了得到更好的性能,对于其它线程来说,可能会出现数据修改的乱序。这么做主要有两个理由。
首先,我们的编译器在生成程序代码时,为了性能,可能让变量在寄存器中存在很长的时间,例如,变量在一个循环中重复使用。如果我们需要这些变量在核心之间可见,那么变量就不能在寄存器分配。在C语言中,可以添加“volatile”关键字达到这个目标。要记住,c/c++中volatile并不能保证让编译器不重排我们的指令。因此,需要使用内存屏障。
排序的第二个主要问题是,一个线程写了一个变量,然后很快读取,有可能从读缓冲中获得比缓存子系统中最新值要旧的值。这对于遵循单写入者原则(Single Writer Principle)的程序来说没有任何问题,但是对于Dekker 和Peterson锁算法就是个很大问题。为了克服这一点,并且确保最新值可见,线程不能从本地读缓冲中读取值。可以使用屏障指令,防止下一个读操作在另一线程的写操作之前发生。在Java中对一个volatile变量进行写操作,除了永远不会在寄存器中分配之外,还会伴随一个完全的屏障指令。在x86架构上,屏障指令在读缓冲排空之前,会显著影响放置屏障的线程的运行。在其它处理器上,屏障有更有效率的实现,例如 Azul Vega在读缓冲上放置一个标志用于边界搜索。
当遵循单写入者原则时,要确保Java线程之间的内存次序,避免store屏障,那么就使用j.u.c.Atomic(Int|Long|Reference).lazySet()方法,而非放置一个volatile变量。
误区
回到作为并发算法中的一部分的“刷新缓存”误区上,我想,可以说我们永远不会在用户空间的程序上“刷新”CPU缓存。我相信这个误区的来源是由于在某些并发算法需要刷新、标记或者清空store缓冲以使下一个读操作可以看到最新值。为了达到这点,我们需要内存屏障而非刷新缓存。
这个误解的另一个可能来源是,L1缓存,或者 TLB,在上下文切换的时候可能需要根据地址索引策略进行刷新。ARM,在ARMv6之前,没有在TLB条目上使用地址空间标签,因此在上下文切换的时候需要刷新整个L1缓存。许多处理器因为类似的理由需要L1指令缓存刷新,在许多场景下,仅仅是因为指令缓存没有必要保持一致。上下文切换消耗很大,除了污染L2缓存之外,上下文切换还会导致TLB和/或者L1缓存刷新。Intel x86处理器在上下文切换时仅仅需要TLB刷新。
(校对注:TLB是Translation lookaside buffer,即页表缓冲;里面存放的是一些页表文件,又称为快表技术,由于“页表”存储在主存储器中,查询页表所付出的代价很大,由此产生了TLB。)
相关推荐
刷新BIOS后,进入3DMark2001再次检查,确认CPU缓存已经正确打开,游戏运行恢复正常。 这个案例提示我们,在遇到类似问题时,不要只关注显卡和驱动程序,也要考虑CPU缓存和主板BIOS的因素。某些系统监控软件(如...
1. 解压下载的"ldkcli5-5.6.0.140 可以刷新网众所有版本缓存.rar"压缩包。 2. 运行"ldkcli5-5.6.0.140.exe",按照说明文件中的提示进行操作。 3. 遵循说明,输入必要的命令参数,执行缓存刷新。 4. 观察系统反馈,...
本文将详细解释图标缓存的工作原理、刷新图标缓存的必要性以及如何使用“大图标.exe”和“小图标.exe”这两个工具进行刷新。 首先,图标缓存是Windows操作系统中的一项机制,它将所有已知图标的图像保存在内存中,...
3. 你会看到“成功刷新DNS解析缓存”的提示,说明操作已完成。 **二、清除Chrome浏览器的DNS缓存** Chrome浏览器也有自己的DNS预解析缓存,为了确保获取最新数据,你可以按照以下步骤进行清理: 1. 打开Chrome...
其次,从 CPU 缓存方面,CPU 缓存对数组友好,因为数组结构是连续的内存, CPU 缓存可以读入一整片连续的内存空间,提高查询效率,而 LinkedList 结构是非连续的内存,不利于 CPU 缓存的使用。 知识点1:ArrayList ...
**CPU高速缓存详解** 在计算机系统中,CPU(中央处理器)是执行程序指令和处理数据的核心部件。为了提高计算效率,现代CPU引入了高速缓存(Cache),它是一种存储技术,用来暂时存储CPU频繁访问的数据,从而减少主...
CPU 缓存的分级 CPU 缓存是现代桌面级 CPU 的一个重要组件,它提供了临时的高速数据存储空间,大大减轻了内存对 CPU 性能的制约。典型的 CPU 缓存结构是由一级缓存(L1 Cache)和二级缓存(L2 Cache)组成,部分...
1. CPU的主频是外频和倍频的乘积,即主频=外频×倍频。 2. 目前CPU的接口主要有Socket和Slot两大类。 3. CPU采用的扩展指令集有Intel公司的SSE、MMX和AMD公司的3DNOW!等几种。 4. CPU的性能指标主要有主频、字长...
3. **使用批处理文件**:根据提供的描述,"重建图标缓存.bat"是一个批处理文件,用于自动化上述过程。双击运行该文件,它会执行必要的命令来重建图标缓存,无需用户手动输入命令。这是非常方便的,尤其是对不熟悉...
功能介绍: 1.网络情球(AFN)常用post,get,上传图片,一次上传多张; 2.数据缓存(YYCache) 3.数据刷新(MJRefresh) 4.网络监测 (Reachability) 5.空白页处理(DZNEmptyData)
1. 分级缓存:对于大比例尺图层,可以考虑使用分级缓存策略,根据不同的缩放级别分层存储,以减少存储空间并优化访问。 2. 异步缓存:Geoserver支持异步生成缓存,这允许在后台进行缓存操作,不会阻塞其他服务。 3...
1. **CPU架构**:CPU的架构分为CISC(复杂指令集计算)和RISC(精简指令集计算)两大类。CISC架构如x86,指令集丰富但执行效率相对较低;RISC架构如ARM,指令集简单,执行效率高,常用于移动设备。 2. **微架构**:...
1. **提高访问速度**:由于缓存位于CPU附近,因此能够快速提供数据,避免了CPU需要等待从更慢的主内存获取数据的情况。 2. **减少延迟**:通过将常用数据保存在缓存中,可以大幅降低数据访问的延迟时间。 3. **提升...
### CPU二级缓存设置详解 #### 一、CPU缓存基础知识 在深入了解如何开启或配置CPU的二级缓存之前,我们需要先对CPU缓存的基本概念有所了解。 **1.1 一级缓存(L1 Cache)** 一级缓存是位于CPU内部的高速缓存,...
Intel和AMD的CPU缓存技术比较 在计算机系统中,CPU缓存是提高系统性能的关键技术。 Intel和AMD是两大CPU制造商,都提供了自己的缓存技术。了解Intel和AMD的CPU缓存技术有何不同,是深入了解计算机系统性能优化的...
5-3JMM-CPU缓存一致性协议MESI.mp4
CPU缓存是计算机硬件系统中的一个重要组成部分,主要目的是解决CPU与主内存速度不匹配的问题,以提高计算机的整体运行效率。缓存(Cache)是一种高速、小容量的存储器,位于CPU与主内存之间,用于暂时存储CPU频繁...
1. **创建缓存** 创建一个Guava Cache对象,你可以通过`LoadingCache`接口,它在缓存项缺失时会自动加载。例如: ```java LoadingCache, Graph> graphs = CacheBuilder.newBuilder() .maximumSize(1000) // 设置...
在“易语言源码易语言刷新文件缓存源码.rar”这个压缩包中,包含的是一段易语言编写的源代码,用于实现文件缓存的刷新功能。下面将详细解析这个知识点。 文件缓存是计算机系统中常见的优化策略,它将频繁访问的文件...
本主题将详细介绍如何利用`mkfontscale`和`mkfontdir`命令来创建和刷新字体缓存,并讨论相关的离线安装包。 首先,让我们了解`mkfontscale`和`mkfontdir`这两个命令的作用: 1. **mkfontscale**:这个命令用于生成...