深入理解存储器层次结构

点击蓝字

8387dd188ca970366889e2b27a0bb338.png

关注我们

95c9de22a322be2edf5b98b6a7c07e39.png

1概述

对于一个简单的计算机系统模型,我们可以将存储器系统看做是一个线性的字节数组,而 CPU 能够在一个常数时间内访问每个存储器的位置。实际上,存储器系统(memory system)是一个具有不同容量、成本和访问时间的存储设备的层次结构。

CPU 寄存器保存着最常用的数据。靠近 CPU 的小、快速的高速缓存存储器(cache memory)做为一部分存储在相对慢速的主存储器(main memory)中数据和指令的缓冲区域。主存缓存存储在容量较大的、慢速磁盘上的数据,而磁盘常常作为存储在通过网络连接的其他机器的磁盘的缓存。

Cache 基本模型

6d5514d175d9fcb36c3a1fff3cbc7d1e.png

问题

CPU 通过总线从主存取指令和数据,完成计算之后再将结果写回内存。这个模型的瓶颈在于 CPU 的超级快的运算速度和主存相对慢的多的运算速度无法匹配,导致大量的时间都浪费在内存上。既然内存比较慢那么就尽量减少 CPU 对内存的访问,于是在 CPU 和 主存之间增加一层 Cache,如下图所示。

b57a0729485bf8d939fb83386e8be035.png

cache

在计算机中,Cache 就是访问速度快的计算机内存被用来保存频繁访问或者最近访问的指令和内存。通常 Cache 的造价比较高,所以相对 Memory 来说,容量比较小,保存的数据也有限。总而言之,由于 CPU 和内存之间的指令和数据访问存在瓶颈,所以增加了一层 Cache,用来尽力消除 CPU 和内存之间的瓶颈。这个模型如下图所示。

1aaee11a25b94d7c0565c0046c860aa0.png

Cache 模型

局部性原理

你可能会问为什么在CPU 和内存之间增加一层 Cache,就可以尽力消除 CPU 和内存之间的瓶颈呢?

3aa503a802b5fa3eeba863d7f371d521.png

why cache work

如上图所示,是局部性原理(principle of locality)让 Cache 更好的工作。一个编写良好的计算机程序通常都具有良好的局部性(locality),程序倾向于引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身,这种倾向性被称作局部性原理。

局部性通常有 2 种不同的形式:时间局部性(temporal locality)和空间局部性 (spatial locality)。在一个具有良好时间局部性的程序中,被引用过一次的内存地址很可能在不远的将来会再被多次引用。在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来会引用附近的一个内存位置。

程序是如何利用这个局部性原理呢?

5edb3928da4e128cd4cb7656481761c3.png

Cache&locality

从数据方面来说,

  1. sum 变量在每次循环迭代的时候都会被访问,符合时间局部性。

  2. 采用步长为 1 的方式访问数组 a ,符合空间局部性。

从指令方面来说,

  1. 循环迭代,符合时间局部性

  2. 线性执行指令,符合空间局部性

对于程序员来说,编写具有良好的局部性的程序是让程序运行更快的方法之一。

存储器的层次结构

7d61b35b65d20dfdb513d9bbc0f51385.png

存储器层次结构

上图展示了一个典型的存储器层次结构。一般而言,从高层往底层走,存储设备变得更慢、更便宜和更大。在最高层是少量快速的CPU 寄存器,CPU 可以再一个时钟周期内访问它们。接下来是一个或者多个小型到中型的基于 SRAM 的高速缓存存储器,可以再几个 CPU 时钟周期内访问它们。

然后是一个大的基于 DRAM 的主存,可以在几十或者几百个时钟周期内访问它们。接下来是慢速但是容量很大的本地磁盘。最后有些系统甚至包括了一层附加的远程服务器上的磁盘,要通过网络来访问它们,例如网络文件系统(Network File System,NFS)这样的分布式文件系统,允许程序访问存储在远程的网络服务器上的文件。

存储器层次结构的核心是,对于每个 k , 位于 k 层的更快更小的存储设备作为位于 k+1 层的更大更慢的存储设备的缓存。也就是说,层次结构中的每一层都缓存来自较低一层的数据对象。例如,本地磁盘作为通过网络从远程磁盘取出文件的缓存,以此类推知道 CPU 寄存器。

64c2fdeab4c9ee7b73c5eae0dfb164fb.pngcache

上图展示了存储器层次结构中缓存的一般性概念。第 k+1 层的存储器被划分成连续的数据对象组块(chunk),称为块(block)。每个块都有一个唯一的名字或者地址以区别其他的块。

例如第 k+1 层存储器被划分成 16 个大小固定的块,编号为 0 ~ 15。第 k 层的存储器被划分成较少的块的集合,每个块的大小与 k+1 层的块的大小一样。在任何时刻,第 k 层的缓存包含了第 k+1 层块的一个子集的副本。例如,第 k 层的缓存有 4 个块的控件,当前包含了 8,9,14,3 的副本。

数据总是以块大小为传输单元在第 k 层 和 第 k+1 层之间来回复制的,虽然在层次结构总任何一对相邻的层次之间块大小是固定的,但是其他的层次对之间可以有不同的块大小。

例如 L1 和 L2 之间的传送通常使用的是几十个个字大小的块,而 L5 和 L4 之间的传送用的是大小为几百或者几千字节的块。一般而言,层次结构中较低层(离 CPU 较远)的设备的访问时间较长,因此为了补偿这些较长的访问时间,倾向于使用较大的块。

13c5d90f3328a7912f77822fcc329835.png

cache hit

当程序需要第 k+1 层的某个数据对象 d 时,它首先会在当前存储在第 k 层的一个块中查找 d。如果 d 刚好缓存在第 k 层,那么就是缓存命中。该程序直接从第 k 层读取 d,根据存储器层次结构的性质,从 k 层读取数据显然比从 k+1 层读取数据更快。如上图所示,一个具有良好时间局部性的程序可以从块 14 中读出一个数据对象,得到一个对 k 层的缓存命中 。

b69df68e0dbba6e3b8c2238f5bd56309.png

cache miss

如果第 k 层中没有缓存数据对象 d,那么就是我们所说的缓存不命中 (cache miss)。当发生缓存不命中时,第 k 层的缓存从第 k+1 层缓存中取出包含 d 的那个块,如果第 k 层的缓存已经满了,那么可能会覆盖现存的一个块。覆盖一个现存的一个块的过程称为替换或者驱逐。被替换的块有时也称作牺牲块。决定替换哪个块是由缓存的替换策略来控制的,替换策略有随机替换和最近最少被使用(LRU)替换策略。

高速缓存存储器

早期的计算机系统的存储器结构只有三层:CPU 寄存器, DRAM 主存,磁盘。由于 CPU 和主存之间逐渐增大的速度差距,系统设计者在 CPU 和 主存之间插入了一个小的 SRAM 高速缓存存储器,称为 L1 高速缓存。随着 CPU 和主存之间逐渐增大的速度差距,系统设计者在 L1 和 主存之间插入了一个更大的 SRAM 高速缓存存储器,称为 L2 高速缓存。

3f1aed13ea513ccf63e0d01a7710270c.png

高速缓存存储器的典型总线结构

假设一个计算机系统,其中每个存储器地址 m 位,形成 M = 2^m 个不同的地址。如下图所示。一个机器的高速缓存被组织成一个有 S = 2^s 个高速缓存组(cache set)的数组。每个组包含 E 个高速缓存行(cache line),每个行由一个 B = 2^b 字节的数据块组成,一个有效位(valid bit)指明这个行是否有效,t = m -(s+b)个标记位(tab bit),他们唯一地标识存储在这个高速缓存行中的块。

b719fb6a1d9a3e978c56b118ca4cc445.png

Cache Organization

根据每个组的高速缓存行数 E,高速缓存可以被分为不同的类,每个组只有一行(E = 1)的高速缓存成为直接映射高速缓存。下面我们以直接映射高速缓存来讲解。

556e940c5afd14bdf33b5c99f4e19de9.png

E=1

假设有这么一个系统,它有一个 CPU,一个寄存器文件,一个 L1 高速缓存和一个主存。当 CPU 执行一条读内存字 w 的指令,它向 L1 请求这个字,如果 L1 有 w 的副本,那么 L1 高速缓存命中,高速缓存取出 w,返回给 CPU。

若是不命中,当 L1 向主存请求包含 w 的块的副本时,CPU 必须等待。当被请求的块从内存到达 L1 时,L1 将这个块存放在它的一个高速缓存行里面,然后取出 w,返回给 CPU 。高速缓存上面的工作过程分为 3 个步骤:

  1. 组选择

  2. 行匹配

  3. 字抽取

第一步,直接映射高速缓存的组选择。高速缓存从 w 中取出 s 个组索引位。例子中的组索引位 00001 定位到组 1。

a5be5b1793a24e82633ff018a1b8ce48.png

直接映射高速缓存的组选择

第二步,直接映射高速缓存的行匹配。由于只有一个高速缓存行,而且有效位也设置了,所以这个行是有用的,从 w 中取出标记位 t ,与高速缓存行中的标记位相匹配,所以缓存命中。

e445a364c11bed728bbaffb2ed2c6486.png

直接映射高速缓存的行匹配

第三步,直接映射高速缓存的字选择。一旦缓存命中,那么我们就知道 w 就在这个块中的某个位置。我们把块看成一个字节的数组,而字节偏移是到这个数组的索引。所以最后一步是确定所需要的字在块中的偏移位置。例子中的块偏移是 100,它说明了 w 的副本是从块中的字节 4 开始的(假设字长为 4 字节)。

第四步,直接映射高速缓存不命中的行替换。如果缓存不命中,那么它需要从存储器层次结构中的下一层取出被请求的块,然后将新的块存储在一个高速缓存行中。对于直接映射高速缓存来说,每个组只要一个行,替换策略就是用新取出的行替换当前的行。

编写高速缓存友好的代码

确保代码高速缓存友好的基本方法有 2 种,

  1. 让最常见的情况运行的快。

  2. 尽量减少每个循环内部的缓存不命中数量。

int sumvec(int v[n])
{int i, sum = 0;for (i = 0; i < N; i++){sum += v[i];} return sum;
}

首先对于局部变量 i 和 sum,循环体有良好的时间局部性。对数组 v 的步长为 1 的引用,对 v[0] 的引用会不命中,而对应的 v[0] ~ v[3] 的块会被从内存加载到高速缓存中,因此接下来的三个引用都会命中,以此类推,四个引用中,三个会命中,这个是我们能做到的最好的情况了,具有良好的空间局部性。

总结

作为一个程序员需要理解存储器的结构层次,因为它对应用程序的性能有巨大的影响。如果你的程序需要的数据是存储在 CPU 寄存器中的,那么在指令的执行期间,在 0 个周期内就可以访问到它们,如果在高速缓存中,需要 4 ~ 75 个周期。

如果存储在主存中,需要上百个周期,如果存储在磁盘上,大约需要几千万个周期。如果理解了系统是如何将数据再存储器层次结构中上上下下移动的,那么就可以在编写自己的应用程序的时候使得他们的数据项存储在结构层次中较高的地方,以便 CPU 可以更快的访问到它们。

编程时候可以注意以下几点,让程序性能更好!

1.重复引用同一个变量的程序有良好的时间局部性;

2.具有步调长度为k的引用模式程序,步调越小,空间局部性越好;

3.循环通常具有很好的空间局部性 & 时间局部性;

4.数组通常具有很好的空间局部性;


参考

本文是华盛顿大学的公开课 《 The Hardware / Software Interface 》的课程笔记,该课程的参考书籍是大名鼎鼎的 CSAPP 也就是《 深入理解计算机系统 》这书。文章截图来源于课程,文章的内容也参考了 CSAPP 的书本内容。

  1. https://courses.cs.washington.edu/courses/cse351/17wi/videos.html

  2. https://book.douban.com/subject/26912767/

*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

6488738633ae7038870c072846010449.png

88675b3a69ad4a86a0e16cdb6cf58704.gif

戳“阅读原文”我们一起进步

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++的一个指针占内存几个字节?

C的一个指针占内存几个字节&#xff1f;结论&#xff1a; 取决于是64位编译模式还是32位编译模式&#xff08;注意&#xff0c;和机器位数没有直接关系&#xff09; 在64位编译模式下&#xff0c;指针的占用内存大小是8字节在32位编译模式下&#xff0c;指针占用内存大小是4字…

【C语言】指针进阶第五站:函数指针!

点击蓝字关注我们函数指针函数也有自己的地址&#xff0c;函数名/&函数名 就是函数的地址1.1基本形式在 数组指针的学习中我们了解到int arr[5]; int (*pa)[5] &arr;//pa是数组指针指针变量pa的类型是int(*)[5]那么函数指针的形式是怎样的呢&#xff1f;void test(cha…

jsp 体检信息查询 绕过用户名验证_一篇彻底搞懂jsp

jsp 实栗 jsp jdbc 实现登录实现思路一个表单页&#xff0c;输入用户登录和密码&#xff0c;然后信息提交到jsp页面进行验证&#xff0c;如果可以服务器跳转到登录成功页&#xff0c;失败&#xff0c;跳转到错误页跳转的时候窗口的URL地址会发生变化代码如下编写登录代码登录&…

C/C++与汇编混合编程有什么好处?

点击蓝字关注我们1 导语 当需要C/C与汇编混合编程时&#xff0c;可以有以下两种处理策略&#xff1a;若汇编代码较短&#xff0c;则可在C/C源文件中直接内嵌汇编语言实现混合编程。若汇编代码较长&#xff0c;可以单独写成汇编文件&#xff0c;最后以汇编文件的形式加入项目中&…

centos 7.6安装java_Hadoop的安装

为了方便后面使用Hadoop的shell命令&#xff0c;我先介绍Hadoop的安装。Hadoop有多种安装模式&#xff0c;这里介绍伪分布式的安装。我测试过Ubutun、Centos和WSL&#xff0c;都可以正常安装Hadoop的所有版本。所有一般不会出现版本对应的问题。Hadoop是基于Java语言进行编写的…

C++软件分析师异常分析工作经验汇总

点击蓝字关注我们最近几年工作当中很大一部分内容是排查软件运行过程中遇到的各种异常&#xff0c;积累了一定的经验&#xff0c;在此给大家分享一下。本文将详细讲述Windows系统中软件异常的分类以及常用的排查方法&#xff0c;给大家提供一个借鉴与参考。1、软件异常的分类常…

java fix_Java中的低延迟FIX引擎

java fix总览 Chronicle FIX是我们的Low Latency FIX引擎和Java数据库。 是什么使它与众不同&#xff1f; 是为Java中的超低GC *设计的。 支持字符串和日期时间的方式可以最大程度地减少垃圾和开销。 可自定义为仅包含您期望的字段。 使用通常在二进制解析器和生成器中使用…

linux 查看防火墙状态_每天五分钟学习Linux系列之 - 系统安全配置

20年IT从业&#xff0c;二哥的团队使用最多的系统就是Linux&#xff0c;开发&#xff0c;运维的小伙伴们都离不开Linux系统&#xff0c;特别是大数据和人工智能领域更是如此&#xff0c;但由于日常工作忙&#xff0c;小伙伴们没有太多成块的时间系统的学习Linux, 并且现版CentO…

C++红黑树模拟实现map和set

点击蓝字关注我们一、红黑树及其节点的设计对于底层都是红黑树的map和set来说&#xff0c;他们之间存在的最大的区别就是&#xff1a;对于set是K模型的容器&#xff0c;而map是KV模型的容器。为了更好的灵活兼容实现map和set&#xff0c;就需要在红黑树以及树节点上进行特别的设…

c语言连接mysql_聊聊数据库MySQL、SqlServer、Oracle的区别,哪个更适合你?

一、MySQL优点&#xff1a;体积小、速度快、总体拥有成本低&#xff0c;开源&#xff1b;支持多种操作系统&#xff1b;是开源数据库&#xff0c;提供的接口支持多种语言连接操作 &#xff1b;MySQL的核心程序采用完全的多线程编程。线程是轻量级的进程&#xff0c;它可以灵活地…

绩效工作流_流绩效–您的想法

绩效工作流上周&#xff0c;我介绍了一些有关Java 8流性能的基准测试结果。 你们和gal足够有兴趣留下一些想法&#xff0c;还有哪些可以介绍。 这就是我所做的&#xff0c;这里是结果。 总览 最后一篇文章的序言也适用于此。 阅读它&#xff0c;以找出所有数字为何撒谎&#…

C语言访问MCU寄存器的两种方式

点击蓝字关注我们单片机的特殊功能寄存器SFR&#xff0c;是SRAM地址已经确定的SRAM单元&#xff0c;在C语言环境下对其访问归纳起来有两种方法。1、采用标准C的强制类型转换和指针来实现采用标准C的强制转换和指针的概念来实现访问MCU的寄存器&#xff0c;例如:#define DDRB (*…

08_优先队列

08_优先队列 一、优先队列最大优先队列最大优先队列API设计 最小优先队列最小优先队列API设计最小优先队列代码实现 索引优先队列索引优先队列实现思路索引优先队列API设计索引优先队列代码实现 一、优先队列 :::info 普通的队列是一种先进先出的数据结构&#xff0c;元素在队…

Python3实现红黑树[下篇]

Python3实现红黑树[下篇]我写的红黑树的上篇在这里&#xff1a;https://blog.csdn.net/qq_18138105/article/details/105190887 这是我近期看的文章 https://www.cnblogs.com/gcheeze/p/11186806.html 我看了很多关于红黑树删除的文章和博客&#xff0c;介绍得是相当相当的复…

C语言内存泄露很严重,如何应对?

点击蓝字关注我们1. 前言最近部门不同产品接连出现内存泄漏导致的网上问题&#xff0c;具体表现为单板在现网运行数月以后&#xff0c;因为内存耗尽而导致单板复位现象。**一方面&#xff0c;内存泄漏问题属于低级错误&#xff0c;此类问题遗漏到现网&#xff0c;影响很坏&…

python发送邮件outlook_通过Python发送Outlook电子邮件?

I am using Outlook 2003. What is the best way to send email (through Outlook 2003) using Python? 解决方案 For a solution that uses outlook see TheoretiCALs answer below. Otherwise, use the smtplib that comes with python. Note that this will require your e…

C++队列queue用法详解(超详细)

点击蓝字关注我们一、定义queue是一种容器转换器模板&#xff0c;调用#include< queue>即可使用队列类。二、queue初始化queue<Type, Container> (<数据类型&#xff0c;容器类型>&#xff09;初始化时必须要有数据类型&#xff0c;容器可省略&#xff0c;省…

python随机抽取人名_python实现艾宾浩斯背单词功能,实现自动提取单词、邮件发送,再也不用担心背单词啦...

&#xfeff;已经完成了利用python爬虫实现定时QQ邮箱推送英文文章&#xff0c;辅助学习英语的项目&#xff0c;索性就一口气利用python多做一些自动化辅助英语学习的项目&#xff0c;对自己的编程能力和英文水评也有一定的帮助&#xff0c;于是在两天的努力下&#xff0c;我完…

用不到125行C语言代码就可以编写一个简单的16位虚拟机?

点击蓝字关注我们一位国外的软件工程师分享了这么一篇博文&#xff1a;Writing a simple 16 bit VM in less than 125 lines of C&#xff08;用不到 125 行 C 语言编写一个简单的 16 位虚拟机&#xff09;。博文地址&#xff1a;https://www.andreinc.net/2021/12/01/writing-…

用一个程序生成另一个程序_还有另一个报告生成器?

用一个程序生成另一个程序如果您具有业务应用程序开发的经验&#xff0c;那么很可能会遇到要求该应用程序具有灵活的报告机制的需求。 我工作的公司主要专注于开发业务解决方案&#xff0c;而报告是必不可少的&#xff0c;实际上&#xff0c;它必须包含我们开发的所有企业系统的…