当前位置:首页 » 硬盘大全 » 缓存未命中怎么办
扩展阅读
webinf下怎么引入js 2023-08-31 21:54:13
堡垒机怎么打开web 2023-08-31 21:54:11

缓存未命中怎么办

发布时间: 2023-02-22 04:36:03

① 什么叫做cache的写命中和写未命中,有什么区别可以通俗解释吗

1、cache的写命中和写未命中,就是磁盘或者内存上的存储区域之前有没有写过数据。

如果有,这次再写到相同的区域叫写命中;

如果写到其他区域,叫写未命中。

2、在数据恢复方面,如果写命中了,那之前的数据被覆盖,就很难再恢复回来;

如果写未命中,那么之前的数据就容易被找回。

(1)缓存未命中怎么办扩展阅读:

缓存命中率

终端用户访问加速节点时,如果该节点有缓存住了要被访问的数据时就叫做命中,如果没有的话需要回原服务器取,就是没有命中。取数据的过程与用户访问是同步进行的,所以即使是重新取的新数据,用户也不会感觉到有延时。 命中率=命中数/(命中数+没有命中数), 缓存命中率是判断加速效果好坏的重要因素之一。

应用场景

是OLTP还是OLAP应用,即使是OLTP,也要看访问的频度,一个极少被访问到的缓存等于没有什么效果。一般来说,互联网网站是非常适合缓存应用的场景。

缓存的粒度

毫无疑问,缓存的粒度越小,命中率就越高,对象缓存是目前缓存粒度最小的,因此被命中的几率更高。

举个例子来说吧:你访问当前这个页面,浏览帖子,那么对于ORM来说,需要发送n条sql,取各自帖子user的对象。很显然,如果这个user在其他帖子里面也跟贴了,那么在访问那个帖子的时候,就可以直接从缓存里面取这个user对象了。

缓存的容量

缓存太小,造成频繁的LRU,也会降低命中率,缓存的有效期太短也会造成缓存命中率下降。

所以缓存命中率问题不能一概而论,一定说命中率很低或者命中率很高。但是如果你对于缓存的掌握很精通,有意识的去调整应用的架构,去分解缓存的粒度,总是会带来很高的命中率的。

    ② php 分页查询怎么redis缓存

    对于有分页条件的缓存,我们也可以按照不同的分页条件来缓存多个key,比如分页查询产品列表,page=1&limit=10和page=1&limit=5这两次请求可以这样缓存查询结果

    proctList:page:1:limit:10

    proctList:page:1:limit:5
    这个是一种常见方案,但是存在着一些问题:

    缓存的value存在冗余,proctList:page:1:limit:10缓存的内容其实是包括了proctList:page:1:limit:5中的内容(缓存两个key的时候,数据未发生变化的情况下)

    仅仅是改变了查询条件的分页条件,就会导致缓存未命中,降低了缓存的命中率

    为了保证数据一致性,需要清理缓存的时候,很难处理,redis的keys命令对性能影响很大,会导致redis很大的延迟,生产环境一般来说禁止该命令。自己手动拼缓存key,你可能根本不知道拼到哪一个page为止。

    放弃数据一致性,通过设置失效时间来自动失效,可能会出现查询第一页命中了缓存,查询第二页的时候未命中缓存,但此时数据已经发生了改变,导致第二页查询返回的和第一页相同的结果。
    以上,在分页条件下这样使用常规方案总感觉有诸多困扰,诸多麻烦,那是不是就应该放弃使用缓存?
    基于SortedSet的分页查询缓存方案
    首先想到的解决方法是使用@see ListOperations<K, V>不再根据分页条件使用多个key,而是使用一个key,也不分页将全部的数据缓存到redis中,然后按照分页条件使用range(key,start,limit)获取分页的结果,这个会导致一个问题,当缓存失效时,并发的写缓存会导致出现重复数据
    所以想到通过使用set来处理并发时的重复数据,@see ZSetOperations<K, V>
    代码逻辑如下:

    range(key,start,limit)按照分页条件获取缓存,命中则直接返回

    缓存未命中,查询(没有分页条件)数据库或是调用(没有分页)底层接口

    add(key,valueScoreMap<value,score>)写入缓存,expire设置缓存时间

    当需要清理缓存时,直接删除key,如果是因为数据新增和删除,可以add(key,value,score)或remove(key,value)

    redis中会按照score分值升序排列map中的数据,一般的,score分值是sql语句的order by filedA的filedA的值,这样能保证数据一致性
    但是这种方式也存在一定问题:

    这个key缓存的value确实是热数据,但可能只有少数数据被频繁使用其余的可能根本就未被使用,比如数据有100页,实际可能只会用到前10页,这也会导致缓存空间的浪费,如果使用了redis虚拟内存,也会有一定影响

    sql查询由原来的分页查询变成了不分页查询,缓存失效后,系统的处理能力较之前会有下降,尤其是对于大表.

    ③ 如何保证数据库缓存的最终一致性

    对于互联网业务来说,传统的直接访问数据库方式,主要通过数据分片、一主多从等方式来扛住读写流量,但随着数据量的积累和流量的激增,仅依赖数据库来承接所有流量,不仅成本高、效率低、而且还伴随着稳定性降低的风险。

    鉴于大部分业务通常是读多写少(读取频率远远高于更新频率),甚至存在读操作数量高出写操作多个数量级的情况。因此, 在架构设计中,常采用增加缓存层来提高系统的响应能力 ,提升数据读写性能、减少数据库访问压力,从而提升业务的稳定性和访问体验。

    根据 CAP 原理,分布式系统在可用性、一致性和分区容错性上无法兼得,通常由于分区容错无法避免,所以一致性和可用性难以同时成立。对于缓存系统来说, 如何保证其数据一致性是一个在应用缓存的同时不得不解决的问题 。

    需要明确的是,缓存系统的数据一致性通常包括持久化层和缓存层的一致性、以及多级缓存之间的一致性,这里我们仅讨论前者。持久化层和缓存层的一致性问题也通常被称为双写一致性问题,“双写”意为数据既在数据库中保存一份,也在缓存中保存一份。

    对于一致性来说,包含强一致性和弱一致性 ,强一致性保证写入后立即可以读取,弱一致性则不保证立即可以读取写入后的值,而是尽可能的保证在经过一定时间后可以读取到,在弱一致性中应用最为广泛的模型则是最终一致性模型,即保证在一定时间之后写入和读取达到一致的状态。对于应用缓存的大部分场景来说,追求的则是最终一致性,少部分对数据一致性要求极高的场景则会追求强一致性。

    为了达到最终一致性,针对不同的场景,业界逐步形成了下面这几种应用缓存的策略。


    1

    Cache-Aside


    Cache-Aside 意为旁路缓存模式,是应用最为广泛的一种缓存策略。下面的图示展示了它的读写流程,来看看它是如何保证最终一致性的。在读请求中,首先请求缓存,若缓存命中(cache hit),则直接返回缓存中的数据;若缓存未命中(cache miss),则查询数据库并将查询结果更新至缓存,然后返回查询出的数据(demand-filled look-aside )。在写请求中,先更新数据库,再删除缓存(write-invalidate)。


    1、为什么删除缓存,而不是更新缓存?

    在 Cache-Aside 中,对于读请求的处理比较容易理解,但在写请求中,可能会有读者提出疑问,为什么要删除缓存,而不是更新缓存?站在符合直觉的角度来看,更新缓存是一个容易被理解的方案,但站在性能和安全的角度,更新缓存则可能会导致一些不好的后果。

    首先是性能 ,当该缓存对应的结果需要消耗大量的计算过程才能得到时,比如需要访问多张数据库表并联合计算,那么在写操作中更新缓存的动作将会是一笔不小的开销。同时,当写操作较多时,可能也会存在刚更新的缓存还没有被读取到,又再次被更新的情况(这常被称为缓存扰动),显然,这样的更新是白白消耗机器性能的,会导致缓存利用率不高。

    而等到读请求未命中缓存时再去更新,也符合懒加载的思路,需要时再进行计算。删除缓存的操作不仅是幂等的,可以在发生异常时重试,而且写-删除和读-更新在语义上更加对称。

    其次是安全 ,在并发场景下,在写请求中更新缓存可能会引发数据的不一致问题。参考下面的图示,若存在两个来自不同线程的写请求,首先来自线程 1 的写请求更新了数据库(step 1),接着来自线程 2 的写请求再次更新了数据库(step 3),但由于网络延迟等原因,线程 1 可能会晚于线程 2 更新缓存(step 4 晚于 step 3),那么这样便会导致最终写入数据库的结果是来自线程 2 的新值,写入缓存的结果是来自线程 1 的旧值,即缓存落后于数据库,此时再有读请求命中缓存(step 5),读取到的便是旧值。


    2、为什么先更新数据库,而不是先删除缓存?

    另外,有读者也会对更新数据库和删除缓存的时序产生疑问,那么为什么不先删除缓存,再更新数据库呢?在单线程下,这种方案看似具有一定合理性,这种合理性体现在删除缓存成功。

    但更新数据库失败的场景下,尽管缓存被删除了,下次读操作时,仍能将正确的数据写回缓存,相对于 Cache-Aside 中更新数据库成功,删除缓存失败的场景来说,先删除缓存的方案似乎更合理一些。那么,先删除缓存有什么问题呢?

    问题仍然出现在并发场景下,首先来自线程 1 的写请求删除了缓存(step 1),接着来自线程 2 的读请求由于缓存的删除导致缓存未命中,根据 Cache-Aside 模式,线程 2 继而查询数据库(step 2),但由于写请求通常慢于读请求,线程 1 更新数据库的操作可能会晚于线程 2 查询数据库后更新缓存的操作(step 4 晚于 step 3),那么这样便会导致最终写入缓存的结果是来自线程 2 中查询到的旧值,而写入数据库的结果是来自线程 1 的新值,即缓存落后于数据库,此时再有读请求命中缓存( step 5 ),读取到的便是旧值。


    另外,先删除缓存,由于缓存中数据缺失,加剧数据库的请求压力,可能会增大缓存穿透出现的概率。

    3、如果选择先删除缓存,再更新数据库,那如何解决一致性问题呢?

    为了避免“先删除缓存,再更新数据库”这一方案在读写并发时可能带来的缓存脏数据,业界又提出了延时双删的策略,即在更新数据库之后,延迟一段时间再次删除缓存,为了保证第二次删除缓存的时间点在读请求更新缓存之后,这个延迟时间的经验值通常应稍大于业务中读请求的耗时。

    延迟的实现可以在代码中 sleep 或采用延迟队列。显而易见的是,无论这个值如何预估,都很难和读请求的完成时间点准确衔接,这也是延时双删被诟病的主要原因。


    4、那么 Cache-Aside 存在数据不一致的可能吗?

    在 Cache-Aside 中,也存在数据不一致的可能性。在下面的读写并发场景下,首先来自线程 1 的读请求在未命中缓存的情况下查询数据库(step 1),接着来自线程 2 的写请求更新数据库(step 2),但由于一些极端原因,线程 1 中读请求的更新缓存操作晚于线程 2 中写请求的删除缓存的操作(step 4 晚于 step 3),那么这样便会导致最终写入缓存中的是来自线程 1 的旧值,而写入数据库中的是来自线程 2 的新值,即缓存落后于数据库,此时再有读请求命中缓存(step 5),读取到的便是旧值。

    这种场景的出现,不仅需要缓存失效且读写并发执行,而且还需要读请求查询数据库的执行早于写请求更新数据库,同时读请求的执行完成晚于写请求。足以见得,这种 不一致场景产生的条件非常严格,在实际的生产中出现的可能性较小 。


    除此之外,在并发环境下,Cache-Aside 中也存在读请求命中缓存的时间点在写请求更新数据库之后,删除缓存之前,这样也会导致读请求查询到的缓存落后于数据库的情况。


    虽然在下一次读请求中,缓存会被更新,但如果业务层面对这种情况的容忍度较低,那么可以采用加锁在写请求中保证“更新数据库&删除缓存”的串行执行为原子性操作(同理也可对读请求中缓存的更新加锁)。 加锁势必会导致吞吐量的下降,故采取加锁的方案应该对性能的损耗有所预期。


    2

    补偿机制


    我们在上面提到了,在 Cache-Aside 中可能存在更新数据库成功,但删除缓存失败的场景,如果发生这种情况,那么便会导致缓存中的数据落后于数据库,产生数据的不一致的问题。

    其实,不仅 Cache-Aside 存在这样的问题,在延时双删等策略中也存在这样的问题。针对可能出现的删除失败问题,目前业界主要有以下几种补偿机制。

    1、删除重试机制

    由于同步重试删除在性能上会影响吞吐量,所以常通过引入消息队列,将删除失败的缓存对应的 key 放入消息队列中,在对应的消费者中获取删除失败的 key ,异步重试删除。这种方法在实现上相对简单,但由于删除失败后的逻辑需要基于业务代码的 trigger 来触发 ,对业务代码具有一定入侵性。


    鉴于上述方案对业务代码具有一定入侵性,所以需要一种更加优雅的解决方案,让缓存删除失败的补偿机制运行在背后,尽量少的耦合于业务代码。一个简单的思路是通过后台任务使用更新时间戳或者版本作为对比获取数据库的增量数据更新至缓存中,这种方式在小规模数据的场景可以起到一定作用,但其扩展性、稳定性都有所欠缺。

    一个相对成熟的方案是基于 MySQL 数据库增量日志进行解析和消费,这里较为流行的是阿里巴巴开源的作为 MySQL binlog 增量获取和解析的组件 canal(类似的开源组件还有 Maxwell、Databus 等)。

    canal sever 模拟 MySQL slave 的交互协议,伪装为 MySQL slave,向 MySQL master 发送 mp 协议,MySQL master 收到 mp 请求,开始推送 binary log 给 slave (即 canal sever ),canal sever 解析 binary log 对象(原始为 byte 流),可由 canal client 拉取进行消费,同时 canal server 也默认支持将变更记录投递到 MQ 系统中,主动推送给其他系统进行消费。

    在 ack 机制的加持下,不管是推送还是拉取,都可以有效的保证数据按照预期被消费。当前版本的 canal 支持的 MQ 有 Kafka 或者 RocketMQ。另外, canal 依赖 ZooKeeper 作为分布式协调组件来实现 HA ,canal 的 HA 分为两个部分:


    那么,针对缓存的删除操作便可以在 canal client 或 consumer 中编写相关业务代码来完成。这样,结合数据库日志增量解析消费的方案以及 Cache-Aside 模型,在读请求中未命中缓存时更新缓存(通常这里会涉及到复杂的业务逻辑),在写请求更新数据库后删除缓存,并基于日志增量解析来补偿数据库更新时可能的缓存删除失败问题,在绝大多数场景下,可以有效的保证缓存的最终一致性。

    另外需要注意的是,还应该隔离事务与缓存,确保数据库入库后再进行缓存的删除操作。 比如考虑到数据库的主从架构,主从同步及读从写主的场景下,可能会造成读取到从库的旧数据后便更新了缓存,导致缓存落后于数据库的问题,这就要求对缓存的删除应该确保在数据库操作完成之后。所以,基于 binlog 增量日志进行数据同步的方案,可以通过选择解析从节点的 binlog,来避免主从同步下删除缓存过早的问题。

    3、数据传输服务 DTS


    3

    Read-Through


    Read-Through 意为读穿透模式,它的流程和 Cache-Aside 类似,不同点在于 Read-Through 中多了一个访问控制层,读请求只和该访问控制层进行交互,而背后缓存命中与否的逻辑则由访问控制层与数据源进行交互,业务层的实现会更加简洁,并且对于缓存层及持久化层交互的封装程度更高,更易于移植。


    4

    Write-Through


    Write-Through 意为直写模式,对于 Write-Through 直写模式来说,它也增加了访问控制层来提供更高程度的封装。不同于 Cache-Aside 的是,Write-Through 直写模式在写请求更新数据库之后,并不会删除缓存,而是更新缓存。


    这种方式的 优势在于读请求过程简单 ,不需要查询数据库更新缓存等操作。但其劣势也非常明显,除了上面我们提到的更新数据库再更新缓存的弊端之外,这种方案还会造成更新效率低,并且两个写操作任何一次写失败都会造成数据不一致。

    如果要使用这种方案, 最好可以将这两个操作作为事务处理,可以同时失败或者同时成功,支持回滚,并且防止并发环境下的不一致 。另外,为了防止缓存扰动的频发,也可以给缓存增加 TTL 来缓解。

    站在可行性的角度,不管是 Write-Through 模式还是 Cache-Aside 模式,理想状况下都可以通过分布式事务保证缓存层数据与持久化层数据的一致性,但在实际项目中,大多都对一致性的要求存在一些宽容度,所以在方案上往往有所折衷。

    Write-Through 直写模式适合写操作较多,并且对一致性要求较高的场景,在应用 Write-Through 模式时,也需要通过一定的补偿机制来解决它的问题。首先,在并发环境下,我们前面提到了先更新数据库,再更新缓存会导致缓存和数据库的不一致,那么先更新缓存,再更新数据库呢?

    这样的操作时序仍然会导致下面这样线程 1 先更新缓存,最后更新数据库的情况,即由于线程 1 和 线程 2 的执行不确定性导致数据库和缓存的不一致。这种由于线程竞争导致的缓存不一致,可以通过分布式锁解决,保证对缓存和数据库的操作仅能由同一个线程完成。对于没有拿到锁的线程,一是通过锁的 timeout 时间进行控制,二是将请求暂存在消息队列中顺序消费。


    在下面这种并发执行场景下,来自线程 1 的写请求更新了数据库,接着来自线程 2 的读请求命中缓存,接着线程 1 才更新缓存,这样便会导致线程 2 读取到的缓存落后于数据库。同理,先更新缓存后更新数据库在写请求和读请求并发时,也会出现类似的问题。面对这种场景,我们也可以加锁解决。


    另在,在 Write-Through 模式下,不管是先更新缓存还是先更新数据库,都存在更新缓存或者更新数据库失败的情况,上面提到的重试机制和补偿机制在这里也是奏效的。


    5

    Write-Behind


    Write behind 意为异步回写模式,它也具有类似 Read-Through/Write-Through 的访问控制层,不同的是,Write behind 在处理写请求时,只更新缓存而不更新数据库,对于数据库的更新,则是通过批量异步更新的方式进行的,批量写入的时间点可以选在数据库负载较低的时间进行。

    在 Write-Behind 模式下,写请求延迟较低,减轻了数据库的压力,具有较好的吞吐性。但数据库和缓存的一致性较弱,比如当更新的数据还未被写入数据库时,直接从数据库中查询数据是落后于缓存的。同时,缓存的负载较大,如果缓存宕机会导致数据丢失,所以需要做好缓存的高可用。显然,Write behind 模式下适合大量写操作的场景,常用于电商秒杀场景中库存的扣减。


    6

    Write-Around


    如果一些非核心业务,对一致性的要求较弱,可以选择在 cache aside 读模式下增加一个缓存过期时间,在写请求中仅仅更新数据库,不做任何删除或更新缓存的操作,这样,缓存仅能通过过期时间失效。这种方案实现简单,但缓存中的数据和数据库数据一致性较差,往往会造成用户的体验较差,应慎重选择。


    7

    总结


    在解决缓存一致性的过程中,有多种途径可以保证缓存的最终一致性,应该根据场景来设计合适的方案,读多写少的场景下,可以选择采用“Cache-Aside 结合消费数据库日志做补偿”的方案,写多的场景下,可以选择采用“Write-Through 结合分布式锁”的方案 ,写多的极端场景下,可以选择采用“Write-Behind”的方案。

    ④ 三级缓存是为读取二级缓存后未命中的数据设计的—种缓存

    首先你要明白缓存的定义,
    缓存的英文是cache,原本是储藏、储藏所、储藏物的意思。
    在计算机科学领域,缓存指的是一组数据的集合,这些数据来自于储存在其他地方或先前计算的结果,而获取或运算出这些数据的代价非常昂贵,为此,把这些结果数据保存起来,让下一次需要这些数据的时候直接使用,而不用重新获取或计算,这就大大提高了系统效率。
    CPU缓存(Cache Memory)位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。由此可见,在CPU中加入缓存是一种高效的解决方案,这样整个内存储器(缓存+内存)就变成了既有缓存的高速度,又有内存的大容量的存储系统了。缓存对CPU的性能影响很大,主要是因为CPU的数据交换顺序和CPU与缓存间的带宽引起的。
    缓存是为了解决CPU速度和内存速度的速度差异问题。内存中被CPU访问最频繁的数据和指令被复制入CPU中的缓存,这样CPU就可以不经常到象“蜗牛”一样慢的内存中去取数据了,CPU只要到缓存中去取就行了,而缓存的速度要比内存快很多。
    这里要特别指出的是:
    1.因为缓存只是内存中少部分数据的复制品,所以CPU到缓存中寻找数据时,也会出现找不到的情况(因为这些数据没有从内存复制到缓存中去),这时CPU还是会到内存中去找数据,这样系统的速度就慢下来了,不过CPU会把这些数据复制到缓存中去,以便下一次不要再到内存中去取。
    2.因为随着时间的变化,被访问得最频繁的数据不是一成不变的,也就是说,刚才还不频繁的数据,此时已经需要被频繁的访问,刚才还是最频繁的数据,现在又不频繁了,所以说缓存中的数据要经常按照一定的算法来更换,这样才能保证缓存中的数据是被访问最频繁的。
    缓存的工作原理是当CPU要读取一个数据时,首先从缓存中查找,如果找到就立即读取并送给CPU处理;如果没有找到,就用相对慢的速度从内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。
    正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在缓存中,只有大约10%需要从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。总的来说,CPU读取数据的顺序是先缓存后内存。
    一级缓存和二级缓存
    为了分清这两个概念,我们先了解一下RAM 。RAM和ROM相对的,RAM是掉电以后,其中的信息就消失那一种,ROM在掉电以后信息也不会消失那一种。
    RAM又分两种,一种是静态RAM,SRAM;一种是动态RAM,DRAM。前者的存储速度要比后者快得多,我们现在使用的内存一般都是动态RAM。
    有的菜鸟就说了,为了增加系统的速度,把缓存扩大不就行了吗,扩大的越大,缓存的数据越多,系统不就越快了吗?缓存通常都是静态RAM,速度是非常的快, 但是静态RAM集成度低(存储相同的数据,静态RAM的体积是动态RAM的6倍), 价格高(同容量的静态RAM是动态RAM的四倍), 由此可见,扩大静态RAM作为缓存是一个非常愚蠢的行为, 但是为了提高系统的性能和速度,我们必须要扩大缓存, 这样就有了一个折中的方法,不扩大原来的静态RAM缓存,而是增加一些高速动态RAM做为缓存, 这些高速动态RAM速度要比常规动态RAM快,但比原来的静态RAM缓存慢, 我们把原来的静态ram缓存叫一级缓存,而把后来增加的动态RAM叫二级缓存。
    一级缓存和二级缓存中的内容都是内存中访问频率高的数据的复制品(映射),它们的存在都是为了减少高速CPU对慢速内存的访问。 通常CPU找数据或指令的顺序是:先到一级缓存中找,找不到再到二级缓存中找,如果还找不到就只有到内存中找了。

    ⑤ 请问“cpu三级缓存是为读取二级缓存后未命中的数据设计的一种缓存”中未命中是指什么具体情况,没看懂

    原本CPU需要的指令和数据都要从内存读取

    但内存太慢,系统就缓存常用的内容到更高速的缓存中

    命中表示所需要的指令/数据在缓存中找到了

    未命中表示所需要的指令/数据不在缓存中,此时需要在下一级缓存中查找,如果都不命中,就只有在内存中查找了

    ⑥ 如何根据代码判断处理器缓存未命中率

    valgrind在C语言编程中,对程序的性能调试,判断一个程序的代码质量是否高效有很明显的用处。下面具体分析一下:
    测试程序的CPU命中率(CPU cache hit/miss rate):
    模拟写两个小程序,用来测试CPU的缓存命中率,分别名为cache1.c和cache2.c.

    cache1.c代码如下:
    点击(此处)折叠或打开
    #include <stdio.h>

    #define MAXROW 8000
    #define MAXCOL 8000

    int main () {

    int i,j;

    static int x[MAXR www.hbbz08.com OW][MAXCOL];

    printf ("Starting!\n");

    for (i=0;i<MAXROW;i++)

    for (j=0;j<MAXCOL;j++)

    x[i][j] = i*j;

    printf("Completed!\n");

    return 0;

    }
    cache2.c代码如下:

    点击(此处)折叠或打开
    #include <stdio.h>

    #define MAXROW 8000
    #define MAXCOL 8000

    int main () {

    int i,j;

    static int x[MAXROW][MAXCOL];

    printf ("Starting!\n");

    for (j=0;j<MAXCOL;j++)

    for (i=0;i<MAXROW;i++)

    x[i][j] = i*j;

    printf("Completed!\n");

    return 0;

    }

    ⑦ 什么是三级缓存

    这就要先了解什么是CPU缓存。简单来说,缓存就是介于CPU核心的寄存器和内存之间的缓冲存储结构。

    CPU的执行单元中有寄存器用于计算,由于是计算使用的,所以不能用于海量存储执行数据,只能是计算时将数据调入,计算完了就输出并清除,准备进行下一次计算。所以寄存器速度虽然快,但是却不能用于长期存储数据。而内存则是CPU堆放临时计算数据的地方(如果需要永久存储则会被放到速度更慢但是容量更大的硬盘里)。但是内存是动态存储器,由于需要刷新动作,虽然容量相对大但是是片外寻址,访问速度比CPU慢。这就需要在CPU寄存器和内存之间建立缓存。缓存为静态存储器。集成度低(缓存会占去CPU上十分可观的一块面积)但速度极高。虽然相比内存来说容量很小,但是速度快了很多。抛开早期的外置结构不谈,目前的CPU缓存都与CPU在同一块芯片上。因此寻址速度也很快。CPU执行程序时先从缓存找数据,遍历缓存后没有找到需要的数据称为没有“命中”。如果缓存未命中,则CPU会转向内存寻找所需数据。

    好了,知道啥是缓存了,那缓存为啥要分一二三级呢?

    因为缓存速度虽然快,但是CPU效率十分高。对缓存的速度还是有要求。而如果缓存越大,遍历一遍寻找数据的速度也就越慢。因此需要对缓存内的数据进行有效的管理。不是最常用的数据就要踢出缓存给最常用的数据腾出空间。这就是缓存分级的原理。缓存的大小需要适度,否则会影响遍历速度。将近几个时钟中最常用的数据保存在一级缓存能大幅提升执行效率。

    不过这只是理论,是牺牲者缓存的一般原理。目前intel处理器采用的是一级缓存是二级缓存的索引目录,二级缓存内存实际数据。这样仅需遍历很小的一级缓存就能知道较大的二级缓存内都保存了哪些数据,是否是CPU需要的。如果一级缓存命中,直接按照地址去二级缓存找。如果一级缓存未命中,也不用遍历二级缓存了,直接去下一级缓存或者内存中寻找了。

    了解以上内容我们就可以了解二级缓存和三级缓存的差异了。正入上面所说,类似于intel的一二级缓存结构仅能服务CPU的一个核心。每个CPU核心都有独立的一级缓存和二级缓存结构。但是CPU内的各个核心有时需要协同工作,这在科学计算等应用中十分常见。这就需要各个核心共享一部分数据。显然私有的一二级缓存不能成为交流媒介。而跑去内存交换效率实在太低了。所以一个各个核心能共享使用的三级缓存就应运而生了。也就是说三级缓存实际上是CPU的各个核心共享的公共缓存结构。

    ⑧ 电脑中的二级缓存有什么用它跟电脑内存的功能各是什么对电脑的性能有什么影响

    更大的二级缓存以及前端总线带宽使得电脑具备了更好的游戏性能。
    在游戏中,L2使得一些图形芯片处理国家流畅,尤其是3D浮点运算能力的增加,最好的例子就是DOOM3的弹道预测!

    更多:
    说到CPU,不得不说的就是CPU缓存,目前CPU的缓存已经成了衡量CPU性能的一个必要指标,那么CPU缓存到底对CPU性能的影响有多大呢?

    我们知道,CPU执行指令时,会将执行结果放在一个叫“寄存器”的元件中,由于“寄存器”集成在CPU内部,与ALU等构成CPU的重要元件,因此寄存器中的指令很快被CPU所访问,但毕竟寄存器的容量太小,CPU所需的大量指令和数据还在内存(RAM)当中,所以CPU为了完成指令操作,需要频繁地向内存发送接收指令、数据。

    由于内存的处理速度远远低于CPU,所以传统的系统瓶颈在这里就产生了,CPU在处理指令时往往花费很多时间在等待内存做准备工作。

    为了解决这个问题,人们在CPU内集成了一个比内存快许多的“Cache”,这就是最早的“高速缓存”。

    L1高速缓存是与CPU完全同步运行的存储器,也就是我们常说的一级缓存,如果CPU需要的数据和指令已经在高速缓存中了,那么CPU不必等待,直接就可以从一级缓存(L1)中取得数据,如果数据不在L1中,CPU再从二级缓存(L2)中提取数据,大大提高了系统的工作效率。

    趣谈CPU缓存工作原理

    没有CPU缓存前

    我们可以形象地把CPU的运算单元想象成是一间坐落在城市中心的工厂,把内存看成是工厂设置在郊区的一间面积很大的仓库A。

    工厂生产所需要的原材料每次都要花时间去远处的仓库A调运,而且到达仓库后,还要等待仓库准备好材料,中间浪费了不少时间。这就是CPU频率未变的情况下,CPU与内存的数据交换不同步的现象。

    而突然有一天,由于资金短缺,仓库A从近郊区“搬到”了远郊区,这样原料和成品在工厂与仓库A之间的运输所花费的时间就更长了,工厂生产所需的原料供应不足,经常处于空运转的状态下。这就是说当CPU频率增加后,CPU与内存交换数据等待需时间会变得更长。

    增加L1Cache

    要解决CPU与内存交换数据不同步这个系统瓶颈问题,其中一个办法是在靠近工厂的市区设置一个小型的仓库B(L1Cache)。

    平时把生产最迫切需要、用得最多的原材料(指令和数据)从仓库A(内存)调配到仓库B(L1Cache),这样工厂生产所需要的原材料就可以很快地调配过来,减少空运转的时间。当所需的原材料在仓库B中找不到(缓存未命中)时,仍然要到仓库A(内存)里调配,虽然无可避免地使工厂又进入空运转,或部分空运转(CPU等待若干个时钟周期),但这样毕竟使等待时间大大降低了。

    小知识:缓存有一个“预读”功能,也就是可以通过一定的算法,猜测接下来所要的数据,并预先取入缓存。

    再添L2Cache

    随着CPU的频率提高,与内存之间交换数据不同步的现象更明显了,可以理解为仓库A(内存)搬离郊区,迁到更远的地方了。解决这一问题的一个更好的办法就是在城市的边缘再设立一个比仓库B大的仓库C,也就是我们说的二级缓存。

    它的作用是把郊区之外的仓库A(内存)中最迫切用的材料(指令)运到仓库C,而工厂如果在仓库B中找不到所需的材料,就可以到仓库C中找,而不必老远跑到仓库A那里找,节省了不少时间。

    通常情况下,L2包括L1所有的数据,另外还有一些附加的数据。换言之,L1与L2、L2与内存之间是子母关系,所以CPU缓存的出现更有效地解决了CPU空等待所造成的资源浪费问题。

    CPU缓存越大越好?

    当然,CPU缓存并不是越大越好,因为缓存采用的是速度快、价格昂贵的静态RAM(SRAM),由于每个SRAM内存单元都是由4~6个晶体管构成,增加缓存会带来CPU集成晶体管个数大增,发热量也随之增大,给设计制造带来很大的难度。所以就算缓存容量做得很大,但如果设计不合理会造成缓存的延时,CPU的性能也未必得到提高

    CPU缓存(Cache Memory)位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。由此可见,在CPU中加入缓存是一种高效的解决方案,这样整个内存储器(缓存+内存)就变成了既有缓存的高速度,又有内存的大容量的存储系统了。缓存对CPU的性能影响很大,主要是因为CPU的数据交换顺序和CPU与缓存间的带宽引起的。

    缓存的工作原理是当CPU要读取一个数据时,首先从缓存中查找,如果找到就立即读取并送给CPU处理;如果没有找到,就用相对慢的速度从内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。

    正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在缓存中,只有大约10%需要从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。总的来说,CPU读取数据的顺序是先缓存后内存。

    最早先的CPU缓存是个整体的,而且容量很低,英特尔公司从Pentium时代开始把缓存进行了分类。当时集成在CPU内核中的缓存已不足以满足CPU的需求,而制造工艺上的限制又不能大幅度提高缓存的容量。因此出现了集成在与CPU同一块电路板上或主板上的缓存,此时就把 CPU内核集成的缓存称为一级缓存,而外部的称为二级缓存。一级缓存中还分数据缓存(Data Cache,D-Cache)和指令缓存(Instruction Cache,I-Cache)。二者分别用来存放数据和执行这些数据的指令,而且两者可以同时被CPU访问,减少了争用Cache所造成的冲突,提高了处理器效能。英特尔公司在推出Pentium 4处理器时,用新增的一种一级追踪缓存替代指令缓存,容量为12KμOps,表示能存储12K条微指令。

    随着CPU制造工艺的发展,二级缓存也能轻易的集成在CPU内核中,容量也在逐年提升。现在再用集成在CPU内部与否来定义一、二级缓存,已不确切。而且随着二级缓存被集成入CPU内核中,以往二级缓存与CPU大差距分频的情况也被改变,此时其以相同于主频的速度工作,可以为CPU提供更高的传输速度。

    二级缓存是CPU性能表现的关键之一,在CPU核心不变化的情况下,增加二级缓存容量能使性能大幅度提高。而同一核心的CPU高低端之分往往也是在二级缓存上有差异,由此可见二级缓存对于CPU的重要性。

    CPU在缓存中找到有用的数据被称为命中,当缓存中没有CPU所需的数据时(这时称为未命中),CPU才访问内存。从理论上讲,在一颗拥有二级缓存的CPU中,读取一级缓存的命中率为80%。也就是说CPU一级缓存中找到的有用数据占数据总量的80%,剩下的20%从二级缓存中读取。由于不能准确预测将要执行的数据,读取二级缓存的命中率也在80%左右(从二级缓存读到有用的数据占总数据的16%)。那么还有的数据就不得不从内存调用,但这已经是一个相当小的比例了。目前的较高端的CPU中,还会带有三级缓存,它是为读取二级缓存后未命中的数据设计的—种缓存,在拥有三级缓存的CPU中,只有约5%的数据需要从内存中调用,这进一步提高了CPU的效率。

    为了保证CPU访问时有较高的命中率,缓存中的内容应该按一定的算法替换。一种较常用的算法是“最近最少使用算法”(LRU算法),它是将最近一段时间内最少被访问过的行淘汰出局。因此需要为每行设置一个计数器,LRU算法是把命中行的计数器清零,其他各行计数器加1。当需要替换时淘汰行计数器计数值最大的数据行出局。这是一种高效、科学的算法,其计数器清零过程可以把一些频繁调用后再不需要的数据淘汰出缓存,提高缓存的利用率。

    CPU产品中,一级缓存的容量基本在4KB到64KB之间,二级缓存的容量则分为128KB、256KB、512KB、1MB、2MB等。一级缓存容量各产品之间相差不大,而二级缓存容量则是提高CPU性能的关键。二级缓存容量的提升是由CPU制造工艺所决定的,容量增大必然导致CPU内部晶体管数的增加,要在有限的CPU面积上集成更大的缓存,对制造工艺的要求也就越高

    ⑨ 计算思维中讲未来可能会被用到的数据存放在高效存储区域中的方式是

    在计算机科学中,缓存是一种高效存储区域,用于存储可能被频繁使用的数据。缓存的作用是通过减少对主存储器(如硬盘或内存)的访问来提高系统性能。
    在计算机系统中,通常有多级缓存系统,其中每一级都比上一级更快,但也更小。例如,CPU可能会有一个内置的缓存,并且内存模块也可能会有一个缓存。
    为了在高效存储区域(如缓存)中存储未来可能会被用到的数据,可以使用一种称为"缓存命中"的机制。缓存命中是指当程序试图访问一个数据项时,缓存会先检查它是否已经存储在缓存中。如果是,则称为"缓存命中",并且程序会从缓存中直接获取数据。如果数据不在缓存中,则称为"缓存未命中",并且程序会从主存储器中获取数据。
    缓存命中的一般策略是将常用的数据存储在缓存中,并将不常用的数据替换出缓存。这样,当程序需要访问常用的数据时,就可以从缓存中快速获取数据,

    ⑩ EMSI协议

    翻译至 https://en.wikipedia.org/wiki/MESI_protocol
    EMSI是基于缓存无效化的一致性缓存协议,并且是一种最常见的支持回写式缓存的协议。它也被称为伊利诺伊州协议(由于其在伊利诺伊大学厄本那香槟分校被开发)。回写式缓存和写入式缓存相比可以节约很多的带宽。回写式缓存经常会存在脏状态,而脏状态表明了高数缓存中的数据与主存中的数据不一致。EMSI要求当缓存未命中时并且别的缓存中有该数据,那么缓存和缓存间应该互相传输数据。MESI相对与MSI来说降低了与主存的交互次数,这带来了显着的性能提升。

    MESI的四个字母分别代表了四个可以被标记在缓存行上的独立状态。(也就是用2bit来编码)

    当缓存行处于Modified状态时,它表明该缓存行只存在于当前缓存中。并且这个缓存行中的数据是脏数据,也就是说这个缓存行中的数据与主存中的数据不一致。缓存被要求在未来将缓存行的数据写于主存中,但不用立即写入。但如果别的缓存向该缓存请求这个数据,那必须保证该数据写入主存已经完成。当回写回主存完成后,缓存行状态会有Modified变为Shared状态。

    当缓存行处于Exclusive状态时,它表明该缓存行只存在于当前缓存中,不过其中的数据与主存中的数据是一致的。当别的缓存向该缓存请求read当前缓存行时,缓存行状态会变成Shared。或者当有write操作时,他会变成Modified状态。

    当缓存行处于Shared状态时,它表明该缓存行可能同时存在与别的缓存中,并且其中的数据与主存中一致。这个缓存行随时可能被丢弃(改变为Invalid状态)。

    当缓存行处于Invalid 状态时,表明该缓存行是无效的。

    对于给定的两个缓存,以下是允许共同存在的状态:

    当一个缓存中的变量被标记为M状态,那同样拥有这个变量的别的缓存的缓存行会被标记为Invalid。

    从一个状态到另一个状态的转变有两个重要影响因素。第一个因素是处理器发出了特殊的读写请求。举个栗子:处理器A的缓存中有变量X,然后这个处理器向自己的缓存发送了对于这个变量的读写请求。第二个影响因素是来自别的处理器的请求,这些处理器缓存中没有这个变量,或者它们想要更新这个变量。这些总线请求被一个名叫Snoopers的监听器监听着。

    下面解释不同种类的处理器请求和总线请求

    处理器请求包括如下两种:

    总线请求包括如下五种:

    如果从主存中获取一个值需要等待的时间比通过缓存间传值等待的时间长,那么我们就可以说缓存间传值可以降低缓存未命中后的读延迟。在多核架构下,一致性主要在二级缓存间保持,但处理器仍然有三级缓存,从三级缓存中获取未命中的变量往往快于从二级缓存。

    监控操作
    所有缓存中的变量都拥有四种状态的有限状态机。
    对于不同输入的状态转换关系和回复如下表1.1和1.2

    只有写操作发生在Modified或Exclusive状态的缓存行时,才缓存才不需要做额外操作。如果状态为Shared的缓存行被写入,那么首先别的缓存需要无效它们的缓存行。这个由一个叫 Request For Ownership (RFO) .的广播操作执行。

    当一个变量为Modified时,这个缓存必须监听所有别的缓存对于该变量对应主存的读请求,一旦监听到就必须将这个变量写入主存。这个过程可以通过强制让别的缓存等待的方法来完成。当完成了对主存的写入,状态变为Shared。但同样可以直接通知别的缓存这个变量的值,而不写入主存。

    当处于Shared状态时,必须监听所有的rfo广播,一旦收到就让缓存中的变量无效。

    Modified和Exclusive状态是精确的,当有缓存行处于这个状态就表明,这个变量是这个缓存独有的。而Shared状态时不精确的,当别的缓存丢弃了这个Shared状态的缓存行,那么可能只存在一个缓存行状态为Shared但它不会变为Exclusive。丢弃Shared状态行不会引起别的缓存的注意。

    使用Exclusive可以带来性能上的提升,因为修改Exclusive不需要通知别的缓存,而修改Shared状态的缓存行需要告诉别的缓存,并使这些缓存行无效,这是耗时的。(这也是EMSI与MSI协议的区别)

    MESI简单直接的实现存在着两个性能问题。1.当向一个Invalid的缓存行写入时,需要从别的缓存获取值,这是很耗时的。2.需要将别的缓存行的该值设置为Invalid这也是耗时的。为了解决这两个问题,我们引入了存储缓冲区和无效化队列。

    当向一个无效的缓存行写入时会用到储存缓冲区。因为这个写操作最终一定会被执行,因此CPU发送读无效消息(让别的缓存中的该缓存行无效)并且将要写入的值放到存储缓存区中,当这个需要的缓存行写入缓存时,存储缓存区中存储的值将被执行写入。
    存储缓存区存在带来的后果是,当一个CPU进行写操作,值不会立即写入缓存中。因此,无论何时CPU读取缓存中的值时,都需要先扫描自己的存储缓冲区,确保缓冲区中是否还有未写入的值。值得注意的时不同CPU之间存储缓冲区时不可见的。

    当CPU收到无效请求时,它会将这写无效请求放入无效化队列中。放入队列中的请求会被迅速执行但不是立即执行。所以CPU可以忽略它的缓存块是无效的。CPU不能扫描它的无效队列,这是和存储缓冲区的区别。

    可见存储缓冲区带来的是写不同步,而无效化队列则带来读不同步,为了解决这些不同步,我们需要内存屏障。写屏障会刷新我们的存储缓冲区,确保里面所有的写都会被执行到这个CPU的cache上,而读屏障可以保证所有无效化队列里的任务都被执行,确保别的CPU的写对自己可见。