CPU缓存那些事儿

CPU缓存那些事儿

CPU高速缓存集成于CPU的内部,其是CPU可以高效运行的成分之一,本文围绕下面三个话题来讲解CPU缓存的作用:

  • 为什么需要高速缓存?
  • 高速缓存的内部结构是怎样的?
  • 如何利用好cache,优化代码执行效率?

为什么需要高速缓存?

在现代计算机的体系架构中,为了存储数据,引入了下面一些元件

  • 1.CPU寄存器
  • 2.CPU高速缓存
  • 3.内存
  • 4.硬盘

从1->4,速度越来越慢,价格越来越低,容量越来越大。 这样的设计使得一台计算机的价格会处于一个合理的区间,使得计算机可以走进千家万户。

由于硬盘的速度比内存访问慢,因此我们在开发应用软件时,经常会使用redis/memcached这样的组件来加快速度。

而由于CPU和内存速度的不同,于是产生了CPU高速缓存。

下面这张表反应了CPU高速缓存和内存的速度差距。

存储器类型时钟周期
L1 cache4
L2 cache11
L3 cache24
内存167

通常cpu内有3级缓存,即L1、L2、L3缓存。其中L1缓存分为数据缓存指令缓存,cpu先从L1缓存中获取指令和数据,如果L1缓存中不存在,那就从L2缓存中获取。每个cpu核心都拥有属于自己的L1缓存和L2缓存。如果数据不在L2缓存中,那就从L3缓存中获取。而L3缓存就是所有cpu核心共用的。如果数据也不在L3缓存中,那就从内存中获取了。当然,如果内存中也没有那就只能从硬盘中获取了。

cache line

对这样的分层概念有了了解之后,就可以进一步的了解高速缓存的内部细节。

高速缓存的内部结构

CPU Cache 在读取内存数据时,每次不会只读一个字或一个字节,而是一块块地读取,这每一小块数据也叫CPU 缓存行(CPU Cache Line)。这也是对局部性原理的运用,当一个指令或数据被拜访过之后,与它相邻地址的数据有很大概率也会被拜访,将更多或许被拜访的数据存入缓存,可以进步缓存命中率。

cache line 又分为多种类型,分别为直接映射缓存多路组相连缓存全相连缓存

下面依次介绍。

直接映射缓存

直接映射缓存会将一个内存地址固定映射到某一行的cache line。

其思想是将一个内存地址划分为三块,分别是Tag, Index,Offset(这里的内存地址指的是虚拟内存)。将cacheline理解为一个数组,那么通过Index则是数组的下标,通过Index就可以获取对应的cache-line。再获取cache-line的数据后,获取其中的Tag值,将其与地址中的Tag值进行对比,如果相同,则代表该内存地址位于该cache line中,即cache命中了。最后根据Offset的值去data数组中获取对应的数据。整个流程大概如下图所示:

cache line

下面是一个例子,假设cache中有8个cache line,

cache line

对于直接映射缓存而言,其内存和缓存的映射关系如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nXbesGIp-1691050559273)(https://raw.githubusercontent.com/zgjsxx/static-img-repo/main/blog/Linux/application-dev/CPU-cache/direct-mapping3.png)]

从图中我们可以看出,0x00,0x40,0x80这三个地址,其地址中的index成分的值是相同的,因此将会被加载进同一个cache line。

试想一下如果我们依次访问了0x00,0x40,0x00会发生什么?

当我们访问0x00时,cache miss,于是从内存中加载到第0行cache line中。当访问0x40时,第0行cache line中的tag与地址中的tag成分不一致,因此又需要再次从内存中加载数据到第0行cache line中。最后再次访问0x00时,由于cache line中存放的是0x40地址的数据,因此cache再次miss。可以看出在这个过程中,cache并没有起什么作用,访问了相同的内存地址时,cache line并没有对应的内容,而都是从内存中进行加载。

这种现象叫做cache颠簸(cache thrashing)。针对这个问题,引入多路组相连缓存。下面一节将讲解多路组相连缓存的工作原理。

多路组相连缓存

多路组相连缓存的原理相比于直接映射缓存复杂一些,这里将以两路组相连这种场景来进行讲解。

所谓多路就是指原来根据虚拟的地址中的index可以唯一确定一个cache line,而现在根据index可以找到多行cache line。而两路的意思就是指通过index可以找到2个cache line。在找到这个两个cache line后,遍历这两个cache line,比较其中的tag值,如果相等则代表命中了。

cache line

下面还是以8个cache line的两路缓存为例,假设现在有一个虚拟地址是0000001100101100,其tag值为0x19,其index为1,offset为4。那么根据index为1可以找到两个cache line,由于第一个cache line的tag为0x10,因此没有命中,而第二个cache line的tag为0x19,值相等,于是cache命中。

cache line

对于多路组相连缓存而言,其内存和缓存的映射关系如下所示:

在这里插入图片描述

由于多路组相连的缓存需要进行多次tag的比较,对于比直接映射缓存,其硬件成本更高,因为为了提高效率,可能会需要进行并行比较,这就需要更复杂的硬件设计。

另外,如何cache没有命中,那么该如何处理呢?

以两路为例,通过index可以找到两个cache line,如果此时这两个cache line都是处于空闲状态,那么cache miss时可以选择其中一个cache line加载数据。如果两个cache line有一个处于空闲状态,可以选择空闲状态的cache line 加载数据。如果两个cache line都是有效的,那么则需要一定的淘汰算法,例如PLRU/NRU/fifo/round-robin等等。

这个时候如果我们依次访问了0x00,0x40,0x00会发生什么?

当我们访问0x00时,cache miss,于是从内存中加载到第0路的第0行cache line中。当访问0x40时,第0路第0行cache line中的tag与地址中的tag成分不一致,于是从内存中加载数据到第1路第0行cache line中。最后再次访问0x00时,此时会访问到第0路第0行的cache line中,因此cache就生效了。由此可以看出,由于多路组相连的缓存可以改善cache颠簸的问题。

全相连缓存

从多路组相连,我们了解到其可以降低cache颠簸的问题,并且路数量越多,降低cache颠簸的效果就越好。那么是不是可以这样设想,如果路数无限大,大到所有的cache line都在一个组内,是不是效果就最好?基于这样的思想,全相连缓存相应而生。

cache line

下面还是以8个cache line的全相连缓存为例,假设现在有一个虚拟地址是0000001100101100,其tag值为0x19,offset为4。依次遍历,直到遍历到第4行cache line时,tag匹配上。

在这里插入图片描述

全连接缓存中所有的cache line都位于一个组(set)内,因此地址中将不会划出一部分作为index。在判断cache line是否命中时,需要遍历所有的cache line,将其与虚拟地址中的tag成分进行对比,如果相等,则意味着匹配上了。因此对于全连接缓存而言,任意地址的数据可以缓存在任意的cache line中,这可以避免缓存的颠簸,但是与此同时,硬件上的成本也是最高。

如何利用缓存写出高效率的代码?

看下面这个例子,对一个二维数组求和时,可以进行按行遍历和按列遍历,那么哪一种速度会比较快呢?

const int row = 1024;
const int col = 1024;
int matrix[row][col];

//按行遍历
int sum_row = 0;
for (int r = 0; r < row; r++) {
    for (int c = 0; c < col; c++) {
        sum_row += matrix[r][c];
    }
}

//按列遍历
int sum_col = 0;
for (int c = 0; c < col; c++) {
    for (int r = 0; r < row; r++) {
        sum_col += matrix[r][c];
    }
}

我们分别编写下面的测试代码,首先是按行遍历的时间:

#include <chrono>
#include <iostream>
const int row = 1024;
const int col = 1024;
int matrix[row][col];

//按行遍历
int main(){
    for (int r = 0; r < row; r++) {
        for (int c = 0; c < col; c++) {
            matrix[r][c] = r+c;
        }
    }
    auto start = std::chrono::steady_clock::now();
    
    //按行遍历
    int sum_row = 0;
    for (int r = 0; r < row; r++) {
        for (int c = 0; c < col; c++) {
            sum_row += matrix[r][c];
        }
    }

    auto finish = std::chrono::steady_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
    std::cout << duration.count() << "ms" << std::endl;
}

标准输出打印了:2ms

接着是按列遍历的测试代码:

#include <chrono>
#include <iostream>
const int row = 1024;
const int col = 1024;
int matrix[row][col];

//按行遍历
int main(){
    for (int r = 0; r < row; r++) {
        for (int c = 0; c < col; c++) {
            matrix[r][c] = r+c;
        }
    }
    auto start = std::chrono::steady_clock::now();
    
    //按列遍历
    int sum_col = 0;
    for (int c = 0; c < col; c++) {
        for (int r = 0; r < row; r++) {
            sum_col += matrix[r][c];
        }
    }

    auto finish = std::chrono::steady_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
    std::cout << duration.count() << "ms" << std::endl;
}

标准输出打印了:8ms

答案很明显了,按行遍历速度比按列遍历快很多。

原因就是按行遍历时, 在访问matrix[r][c]时,会将后面的一些元素一并加载到cache line中,那么后面访问matrix[r][c+1]matrix[r][c+2]时就可以命中缓存,这样就可以极大的提高缓存访问的速度。

如下图所示,在访问matrix[0][0]时,matrix[0][1]matrix[0][2]matrix[0][2]也被加载进了高速缓存中,因此随后遍历时就可以用到缓存。

cache line

而按列遍历时,访问完matrix[0][0]之后,下一个要访问的数据是matrix[1][0],不在高速缓存中,于是需要再次访问内存,这就使得程序的访问速度相较于按行缓存会慢很多。

参考文章

https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESIHelp.htm
https://cloud.tencent.com/developer/article/1495957
https://www.6hu.cc/archives/79496.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/57741.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Go学习第三天

map的三种声明定义方式 声明map后&#xff0c;一定要make开辟空间&#xff0c;否则会报越界且不能使用 package mainimport "fmt"func main() {// 第一种声明方式// 声明myMap1是一种map类型 key是string value是stringvar myMap1 map[string]string// 判断一下map在…

小研究 - 微服务系统服务依赖发现技术综述(二)

微服务架构得到了广泛的部署与应用, 提升了软件系统开发的效率, 降低了系统更新与维护的成本, 提高了系统的可扩展性. 但微服务变更频繁、异构融合等特点使得微服务故障频发、其故障传播快且影响大, 同时微服务间复杂的调用依赖关系或逻辑依赖关系又使得其故障难以被及时、准确…

木马病毒怎么回事?带你深度分析了解木马病毒!

一、病毒简介 SHA256:3110f00c1c48bbba24931042657a21c55e9a07d2ef315c2eae0a422234623194 MD5:ae986dd436082fb9a7fec397c8b6e717 SHA1:31a0168eb814b0d0753f88f6a766c04512b6ef03 二、行为分析 老套路&#xff0c;火绒剑监控&#xff1a; 这边可以看见创建了一个exe&#x…

性能压力测试的重要性与实施方法

性能压力测试是在软件开发过程中评估系统在不同负载条件下的表现和稳定性的关键步骤。这种测试是为了确定系统在正常和峰值负载下的性能表现&#xff0c;以验证系统是否能够满足用户需求&#xff0c;同时发现潜在的性能问题并加以解决。 首先&#xff0c;性能压力测试对于确保系…

《皮囊》阅读笔记

《皮囊》阅读笔记 2023年8月2号在杭州小屋读完&#xff0c;该书共收录14篇散文&#xff0c;内容大致分为两部分&#xff1a;前半部分讲述作者的阿太&#xff08;外婆的母亲&#xff09;、母亲、父亲关于生活哲学、房子、疾病与信仰的故事&#xff0c;后半部分讲述生活在小镇的张…

【BASH】回顾与知识点梳理(一)

【BASH】回顾与知识点梳理 一 前言一. 认识与学习 BASH1.1 硬件、核心与 Shell1.2 为何要学文字接口的 shell&#xff1f;1.3 系统的合法 shell 与 /etc/shells 功能1.4 Bash shell 的功能1.5 查询指令是否为 Bash shell 的内建命令&#xff1a; type1.6 指令的下达与快速编辑按…

122.买卖股票的最佳时机2

一、题目 122. 买卖股票的最佳时机 II - 力扣&#xff08;LeetCode&#xff09; 二、代码 class Solution { public:int maxProfit(vector<int>& prices) {int n prices.size();vector<vector<int>>dp(n,vector<int>(2,0));//0表示第i天不持有股…

python算法指南程序员经典,python算法教程pdf百度云

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;你也能看懂的python算法书 pdf&#xff0c;python算法教程这本书怎么样&#xff0c;现在让我们一起来看看吧&#xff01; 给大家带来的一篇关于算法相关的电子书资源&#xff0c;介绍了关于算法、详解、算法基础方面的内…

最细致讲解yolov8模型推理完整代码--(前处理,后处理)

研究yolov8时&#xff0c;一直苦寻不到Yolov8完整的模型推理代码演示&#xff0c;大部分人都是基于Yolo已经封装好的函数调用&#xff0c;这个网上教程很多&#xff0c;本文就不赘述这方面的内容了&#xff0c;接下来将细致全面的讲解yolov8模型推理代码&#xff0c;也就是yolo…

cloudstack远程调试

前置条件&#xff1a;服务器安装好cloudstack的management、agent; 1、managemeng、agent启动服务文件 packaging/systemd cloudstack-agent.default # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTIC…

ES6之Promise、Class类与模块化(Modules)

目录 PromiseClass类extendssuper Modules 模块系统export default 和对应importexport 和 import Promise Promise 是 ES6 引入的一种用于处理异步操作的对象。 它解决了传统回调函数&#xff08;callback&#xff09;模式中容易出现的回调地狱和代码可读性差的问题。 Promis…

数学学习——最优化问题引入、凸集、凸函数、凸优化、梯度、Jacobi矩阵、Hessian矩阵

文章目录 最优化问题引入凸集凸函数凸优化梯度Jacobi矩阵Hessian矩阵 最优化问题引入 例如&#xff1a;有一根绳子&#xff0c;长度一定的情况下&#xff0c;需要如何围成一个面积最大的图像&#xff1f;这就是一个最优化的问题。就是我们高中数学中最常见的最值问题。 最优化…

小白到运维工程师自学之路 第六十五集 (docker-compose)

一、概述 Docker Compose 的前身是 Fig&#xff0c;它是一个定义及运行多个 Docker 容器的工具。可以使用YAML文件来配置应用程序的服务。然后&#xff0c;使用单个命令&#xff0c;您可以创建并启动配置中的所有服务。Docker Compose 会通过解析容器间的依赖关系&#xff08;…

vmware网络配置

效果&#xff1a; 虚拟机和物理机网络互通&#xff1b; 虚拟机可以上外网 环境&#xff1a; vmware version 17.0.0 Centos 7.9 配置 1&#xff0c;vmware 菜单 - 编辑 - Virtual Network Edit 2&#xff0c; 选择VMnet8 VMnet information:NAT&#xff1b; 勾选2个…

ansible控制主机和受控主机之间免密及提权案例

目录 案例描述 环境准备 案例一--免密远程控制主机 效果展示&#xff1a; 解决方案 1.添加主机 2.通过ssh-key生成密钥对 3.生成ssh-copy-id 4.验证 案例二-----免密普通用户提权 效果展示 解决方案 1.使用普通用户&#xff0c;与案例一 一样&#xff0c;进行发送密钥…

【力扣每日一题】2023.8.2 翻转卡片游戏

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 这道题不是什么翻转卡片游戏&#xff0c;这就是纯纯的文字游戏&#xff0c;要是能看懂题目那就是非常简单&#xff0c;接下来我就给大家分…

低代码已经发展到什么水平了?

在数字化转型的浪潮下&#xff0c;企业和组织迫切需要更快速、高效的应用开发方式来满足日益复杂的业务需求。而低代码开发作为一种创新的开发方式&#xff0c;正在引领着应用开发的新潮流。低代码开发允许开发者以可视化的方式快速构建应用&#xff0c;减少了繁琐的代码编写&a…

android 如何分析应用的内存(十五)——Visual Studio Code 调试Android应用

android 如何分析应用的内存&#xff08;十五&#xff09;——Visual Studio Code 调试Android 应用 在上一篇文章介绍了jdb调试java应用 接下来介绍用UI界面调试java应用&#xff0c;达到同jdb一样的效果。 同样的UI界面有很多选择&#xff0c;如Eclipse&#xff0c;Android …

数论分块学习笔记

准备开始复习莫比乌斯反演&#xff0c;杜教筛这一部分&#xff0c;先复习一下数论分块 0.随便说说 数论分块可以计算如下形式的式子 ∑ i 1 n f ( i ) g ( ⌊ n i ⌋ ) \sum_{i1}^{n}f(i)g(\lfloor\frac{n}{i}\rfloor) ∑i1n​f(i)g(⌊in​⌋)。 利用的原理是 ⌊ n i ⌋ \lf…

GoogLeNet卷积神经网络-笔记

GoogLeNet卷积神经网络-笔记 GoogLeNet是2014年ImageNet比赛的冠军&#xff0c; 它的主要特点是网络不仅有深度&#xff0c; 还在横向上具有“宽度”。 由于图像信息在空间尺寸上的巨大差异&#xff0c; 如何选择合适的卷积核来提取特征就显得比较困难了。 空间分布范围更广的…