深入理解计算机系统(第三版)笔记
第一章:计算机系统漫游
1.从hello.c了解高级程序语言的编译系统
hello程序的生命周期从一个高级的C语言程序开始的。然而,为了在系统上运行hello.c程序,系
统是不理解的,需要通过编译系统编译成低级的机器指令才能运行。
Ps:不同CPU的指令集等不同,机器指令也不同,所以一般可执行文件由对应的系统生成
1.1编译系统的不同阶段
这里把.c->.i的过程成为一个完整阶段
预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include<...>
告诉预处理器读取系统头文件的内容,并 _直接插入_ 到程序文本中。结果得到另一个C语言程序
但是是以.i作为扩展名。
编译阶段:编译器将文本文件hello.i翻译成文本文件hello.s,它包含了一个汇编语言程序。
汇编阶段:汇编器将文本文件hello.s翻译成_机器语言指令_,将其打包成一种叫做可重定位目标
程序.o的格式,并将结果保存在目标文件hello.o文件中,该文件是一个二进制文件。
链接阶段:这个hello程序调用了库函数printf,该函数存在于一个名为printf.o的文件中,这
个函数不必再次编译,链接器只需要以某种方式将.o库函数链接到此.o文件,节省了存储可能副本
的空间,和编译的时间!
2.计算机系统系统的硬件组成
了解到了我们的程序是如何转变为硬件能识别的操作,接下来需要了解系统运行.o可执行文件的硬件
总线:贯穿整个系统的电子管道,接待信息在负责在各个部件间传递。
I/O设备:每一个I/O设备通过一个控制器或适配器与I/O总线相连(Ps:控制器是I/O设备本身或
者系统的主硬刷电路板;适配器则是一块插在主板插槽的卡)
主存:一个临时存储设备。主存是由一组动态随机存取存储器(DRAM)芯片组成的。
(Ps:逻辑上看是一个线性的字节数组,每个字节都有自己唯一的地址索引)
处理器CPU:CPU的指令集架构能够抽象出功能模型如下
1、加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容
2、存储:从寄存器复制一个字节或者一个字的主存的某一个位置,以覆盖这个位置的内容
3、操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算术运算,并将结果存放在一个
寄存器中,覆盖原寄存器的内容
4、跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,覆盖PC中原来
的值
hello.o的输入输出(细节已被忽略,可以理解为“hello world”这个数据的走向)
3.并行和并发
从高到低来区分并行和并发理解
线程级并发:也就是传统意义上的并发,这种并发执行只是模拟出来的,是通过计算机在执行的进程
或者线程间快速切换实现的。(另一方面并行就是同时执行)
Ps:_超线程_也称同时多线程,能实现超线程的CPU与传统的相比,线程切换由原来的20000个时钟
周期做切换变为1个时钟周期做切换。那么4核8线程可以并行的执行8个线程。(P18)
指令级并行:CPU同时执行多条指令的属性成为指令级并行
单指令、多数据并行:单从一个指令来说,一个指令也可以拆分为多个可以并行执行的操作,即
SIMD并行。如处理器并行处理8对单精度浮点数(float)做加法的指令。
第二章:信息的表示和处理
本章主要是讲的int、float等的存储和运算过程
1.常见数据类型
C声明(无符号和有符号占用相同) | 32位系统字节数 | 64位系统字节数 |
---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 8 |
int32_t | 4 | 4 |
int64_t | 8 | 8 |
char * | 4 | 8 |
float | 4 | 4 |
double | 8 | 8 |
注意:我们称的“32位程序”或者“64位程序”,区别在于程序如何编译的,而不是运行的系统!!
2.大小端字节存储
理解大端和小端时,要确定这是对高字节序和低字节序的排列,而不是一个字节的排序方向!!
现在从十六进制数据的存储来理解:0x01234567
可以把0x100为主存中的一个独立数组的开头,开头是数据低位的是小端,开头是高位的为大端。
(Ps:数组等数据结构的寻址不受影响!!!)
(Ps:主机一般是小端字节序,网络传输是大端字节序)
3.逻辑右移和算数右移
左移必定是_逻辑左移_,也就是常见的补0移位。
char x | x>>4(逻辑右移) | x>> 4(算数右移) |
---|---|---|
0x01100011 | 0x_0000_0110 | 0x_1111_0110 |
Ps:算数右移和逻辑右移不同机器/编译器的选择不一样!!!
4.计算机中的浮点数
计算机中的整数存储和运算较为简单,本节只涉及浮点数的总结
4.1浮点数的内存结构
Ps:float的exp偏移量为127;
double的exp偏移量为1023;
即float需要表示2^-3时,exp=127+(-3);表示2^2时,exp=127+2;
4.2浮点数的规格化
规格化:1
非规格化:2
特殊值:3a、3b==>3a的s为0为正无穷,反之负无穷;3b全称Not a number。
4.3 浮点数的舍入
整数遇到被除数小于除数的时候,会直接舍去掉,为0;那么浮点数是怎么做的呢?
对于浮点数来说,提供了四种舍入方式:
1、向偶数摄入(也称向最接近的值取整)————默认;==注意1.5是取2.0==
2、向零舍入(看正负)
3、向下舍入(变小)
4、向上舍入(变大)
Ps:向偶数取整是最符合统计学的取证方式————向上取整和向下取整的概率为0.5
4.4浮点数的运算
4.4.1浮点数的加法不具有结合性所引发的对浮点数精度的思考
x = 3.14+pow(1,10)-pow(1,10);=======>3.14
x = 3.14+(pow(1,10)-pow(1,10));======>0
这里回顾一下小数部分是如何转换为二进制的:
很明显,这种情况存在存储空间不足以存储那么多为二进制小数,所以造成了过长的小数部分被放弃存储;
举个很简单的例子**注意理解float存储**:
float i = 10000000000;
cout<<i;
//那么将会输出10000000000
//原因在于他在内存中的数据部分为1001010100000010111110010000000000(1+23位+10位)
//相当于保存的前24位,100101010000001011111001(其中保存浮点数第一位为1时保存,故只存放后23位)
//当再次取出这个数9765625*pow(2,10)= 10,000,000000;是满足的
//但是换一种情况:
float j = 10000000001;
cout<<j;
//很明显,无法保存最后一位,最后结果是 10,000,000000;
Ps:这里补充一下对浮点数精度的理解,这里的精度是对frac部分的最小步长,float的精度为pow(2,-23)=0.00000119..
为_小数点后第6位_,所以float的精度的理解和物理意义上的尺子的精度是同一概念。
那么double的精度呢?
同理:2*pow(2,-52) = 2.22..e-16,故精度为_小数点之后第16位_。
那么有一个问题来了,为什么又说float和double的精度为5和15呢?
我是这么理解的,这是由于他们本身不是并不是完全整除,为了保证数太小误差太大,所以少取一个有效位!
4.5C语言中的浮点数
C语言中不要求机器使用IEEE浮点,所以没有标准的方法来改变舍入方式,也没有-0,NaN之类的特殊值,所以系统提供了.h
文件和读取这些特征的过程库来帮助控制浮点数。
第三章:程序的机器级表达(x86-64)
明确高级语言编写的程序能在不同的机器上汇编执行,而汇编代码则需要机器支持其操作
为了方便降低初学时阅读汇编代码的难度我们可以通过如下Unix命令进行编译得到执行文件:
linux>gcc -Og -o t t.c
linux>gcc -Og -S t.c (直接生成)
获得了0级优化的代码之后,再通过objdump工具反汇编得到汇编代码
linux>objdump -d t (反汇编生成,个人认为此项生成的更适合初学者)
1.机器级代码
机器级代码的抽象概念:
1、指令集体系结构或指令集架构
(定义了处理器状态、指令的格式,以及每条指令对状态的影响)
2、虚拟级程序使用的地址是虚拟地址
(多个硬件存储器和操作系统软件组合起来)
1.1指令集架构
处理器状态:
1、程序计数器(称为“PC”,在x86-64中用%rip表示),保存将要执行的下一条指令的内存地址
2、整数寄存器(16个命名的位置,分别存储的64位的值)
3、条件码寄存器(保存最近执行的算术和逻辑指令的状态信息)
4、向量寄存器(可以存放一个或多个整数;或者浮点数)
Ps:
1)汇编代码_不区分_有符号数、无符号数;_不区分_不同类型的指针;_不区分_指针、整数。
2)汇编代码只执行一个很基本的操作,例如寄存器两数加减,存储器和寄存器之间传递数据,条件寄存器传递新的指令;
补充整数寄存器(也称通用目的寄存器),这是汇编代码中最容易被调用的寄存器之一
这里的栈空间%rbp存放的同一级栈空间的最高位,%rsp保存的则是栈底
1.2汇编代码的数据格式
数据类型在x86-64机器中的大小
ps:intel用字节(byte)代表8位,用字(word)代表16位,双字代表32位(double word),
四字代表64位(quad word)
C声明 | 64位系统字节数 | 大小(字节) | 汇编代码后缀 |
---|---|---|---|
char | 字节 | 1 | b |
short | 字 | 2 | w |
int | 双字 | 4 | d或者l |
long | 四字 | 8 | q |
char * | 四字 | 8 | q |
float | 单精度 | 4 | s |
double | 双精度 | 8 | l |
注意这里的双字是可以用l表示,并且和双精度的l相比在使用的时候不会有歧义,因为浮点数操作的寄存器和指令都是不同的。
1.3数据格式不同存在方式(操作数)
1、立即数:用来表示常量
2、寄存器:用来表示某个寄存器的内容
3、内存引用:它会根据计算出来的地址(通常成为有效地址)访问某个内存地址
1.4数据操作指令
1.4.1数据传送指令
使用最频繁的指令就是将数据从一个位置复制到另一个位置的指令
1.4.1.1数据保持不变的传送到目的地址(不改变数据大小、符号)
Ps:1、mov指令的顺序从左到右,如mov a,b,则把a的值复制给b
2、movq虽可传四字,但一旦要传立即数,则只能传32位补码表示的立即数,随后把它符号拓展到64位。
而movabsq可以直接传64位的立即数,但是它只能以_寄存器_作为目的地。
3、所有mov指令都不支持从一个内存地址直接传到另一个内存地址,如movw (%rax),4(%rsp)是不行的。
4、决定mov使用哪个后缀的是寄存器的大小,当两边操作的都是寄存器时,若大小不同,必须用小数据复制到大目的地的
类型的mov指令(接下来就会讲到),当两边操作的是立即数和内存时,可以以立即数大小为准。
例子:movl $0x4050,%eax //0x4050虽然是2字节,但%eax是4字节,所以movl
movw %bp,%sp
movb (%rdi,%rcx),%al
movb $17,(%rsp) 立即数->内存
movq %rax,-12(%rbp)
1.4.1.2数据长度变化传送到目的地址
总的分为两类:1、做0扩展 2、做符号扩展;
Ps:数据传输只有数据扩展
整个扩展目标地址只能是寄存器,源地址可以是内存、也可以是寄存器
通过零扩展传输数据:
注意:观察到数据长度由短变长的操作,并没有movzlq,这是由于x86-64的很特殊的规定。
即:movl操作会自动将高位置零,这也方便了系统在32位和64位的切换。
通过符号扩展传输数据:
1.4.1.3压入和弹出栈数据
Ps:可以看到pushq以及popq的效果看上去是两条指令的组合;
实际上在机器代码中pushq和popq仍然只有1个字节的编码。
1.4.2数据运算
注意:这里的除了leaq操作,其余操作都未添加后缀,需要看操作什么数据
1.4.2.1加载有效地址
加载有效地址的指令leaq实际上是movq的一种变形,区别在于:
movq:传输的数据本身
leaq:传输的数据所在的有效地址(即偏移地址),类似于地址操作符&x
1.4.2.2运算的一元操作和二元操作(和数据移动的有区别!!)
1、操作数为1的是一元操作符,既是源也是目的;==这个操作可以是内存,也可以是寄存器,和mov不一样==
2、操作数为2的是二元操作符,如subq %rdi,%rax操作,%rdi和%rax都是源操作数,%rax是目的操作数
Ps:值得注意的是,目的操作数都是内存时,实际上需要从内存读出对应位置的数操作后再放回(暗含两次数据传输操作)
补充:
常常看到的(%rax,%rdx) =====> %rax+%rdx
另外的-4(%rax,%rdx,2)======> %rax+%rdx*2-4
1.4.2.3移位操作
移位操作是二元操作,复合运算的二元操作的特征,并且与高级语言相比,对逻辑右移和算数右移的操作做出了区分;
(或许也可以通过汇编代码推测出这台机器上的编译系统的特点)
1.4.2.4特殊算术操作
这里的imulq指令与乘法运算imulq的汇编指令相同,但不同的是这里的imulq是_一元操作_:
当需要获得两个64位数的64位乘积时,使用_二元的imulq即可_
当需要获得两个64位数的128位乘积时,使用_一元的imulq即可_(看着是一元,实际上对两个寄存器进进行操作)
另外,有前缀i表明进行操作的是有符号数进行运算,无前缀i说明的是无符号数进行运算
1.5控制
诸如C语言的条件语句、循环语句、分支语句,它们的汇编实现就需要
1.5.1条件码
较为常见的条件码如上。
Ps:每当需要数据或者地址(leaq)===计算===的操作,都会为其指令设置条件码!!!
条件码通常不会直接读取,常用的使用方法;
1、可以根据条件码的某种组合,将一个字节设置为0或者1(SET指令)
2、可以条件跳转到程序的某个其他的部分(jump指令)
3、通过条件码传送数据
1.5.2比较和测试指令(结果改变的是条件码)
1、观察cmp的比较操作,其实可以发现本质上是两数相减,观察结果的正负,从而设置条件码
2、另外,test同时可以等效于两数的位与
1.5.3SET指令(根据条件码设置置0置1一字节的数据位置)
1.5.1提到,条件码能够通过设置条件码的某种组合来将_一个字节_ (关键:set的后缀不是置零的位数,而是判断的关系符)
1.5.4跳转指令
跳转指令会导致执行切换到程序的一个全新的位置
1.5.4.1jump指令(条件控制转移的传统方法:循环也是这种方式实现的)
直接跳转:无条件调换,跳转目标为具体的地址
间接跳转:跳转到一个保存的地址位置
如jmp *%rax //跳转到%rax保存的值作为的地址
如jmp *(%rax) //跳转到%rax保存的地址中保存的地址的位置(类似于双指针)
注意,这里除了第一第二条指令是肯定要跳转的,其余需要判断条件码的!!(类似于SET)
并且jump指令是实现C语言中的goto指令的重要汇编指令!!!!!
可以发现,1是向高地址进行跳转,8则是向地址值进行跳转。
Ps:这是实现条件操作的传统方法,当条件满足时,沿着一条路径一直执行下去(.L4),当条件不满足时,走另一条路径。
!!一种替代的策略是使用数据的条件转移,这种方法计算一个条件操作的两种结果,然后根据条件是否满足其中的一
个,通过数据传送执行——也就是接下来的条件传送指令。
补充——为什么基于条件传送的代码会比基于条件控制转移 性能能要好?
原因在于对于现代处理器来说,是通过使用流水线的方式来获得高性能。所以对于处理器指令集来说,一条指令大多要经历
一系列的过程(如:从内存取指令,读数据,算数运算等等)。流水线的这种方法通过使流水线充满待执行的指令能达到最
好的性能。
了解了处理器指令最好的状态,再回过头来区分条件传送和条件控制转移,处理器是通过分支预测逻辑的方式(这里的预测成功
率都为90%以上),将猜测的一个条件的指令放入流水线,但是错误的条件传送会导致丢弃该传送的所有指令(15-30个时钟
周期),而条件传送则不用花费这么多时间,所以从性能上条件传送更优。
1.5.4.2条件传送(带条件的mov,一般需要提前对条件码进行设置)
通过条件传送指令,处理器无需预测测试的结果,而是当执行到该语句时,读取源值,检查条件码,然后要么更新_目的寄存器_,
要么保持不变。
缺点:
条件传送只能说是提供了一种条件控制转移来实现条件操作的替代策略,它的使用也只能在受限的条件下。比如说,如
果传送的条件是通过空指针判断的,那么一定涉及把用来判断的指针进行引用,即(movq (%rdi),%rax),这导致了间接
引用空指针的问题。所以这种情况下只能使用条件控制的方式。
1.5.5实现
for/while循环以及if等语句都是通过条件控制转移的方法实现的!!!
这里不做赘述
1.6过程
过程是软件中一种重要的概念。
提供一种封装代码的方式,用一组指定的_参数_和一个可选的_返回值_实现了某种功能。
Ps:是不是感觉过程像极了高级语言的函数。
实际上,函数就是高级语言的过程,不同的编程语言过程的形式多种多样:
1、函数
2、方法
3、子例程
4、处理函数等等
类比完过程在不同编程语言中的过程,回过头来看一下过程:
假设过程P调用过程Q,Q执行后返回P,需要经历下面几个机制;
1、传递控制:调用过程Q时,程序计数器(PC)切换到过程P代码的起始地址
返回过程P时,程序计数器(PC)切换到过程Q调用过程P的后一指令
2、传递数据:向过程Q提供一个或者多个参数,Q必须_能向_P返回一个值
--CPU提供了6个寄存器保存参数,若参数多于6个,则需要在调用Q时,P为其准备好
3、分配和释放内存:过程开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些储存空间
1.6.1运行时的栈(分配和释放内存)
仍然是以过程P调用过程Q,Q执行后返回P的例子,观察运行时的栈的区别。
1、当Q运行时,它只需要为局部变量分配新的存储空间,或者设置到另一个过程的调用。
2、当Q返回时,任何被他分配的局部变量都将被返回。
3、程序可以用栈来管理它的过程所需的
——存储空间,栈和程序寄存器存放着传递控制和数据、分配内存所需要的信息。
4、当P调用Q时,控制和数据信息添加到栈尾,当p返回时,这些信息会释放掉(被重新调用)。
Ps:返回地址指明了返回Q返回到过程P时,从过程P的哪个位置继续执行(也就是执行的P调用Q的下一条指令)!!
1.6.2传递控制
传递控制这项操作底层的工作是:从过程P转移到过程Q只需要简单的将程序计数器(PC)设置为Q代
码的起始位置,返回则是将程序计数器设置为返回地址(栈空间的其他类型地址不属于控制)。
当在x86-64的计算机里进行反汇编指令时,常常看到callq和retq的指令表示原来的call和ret,
这里的q并不表示操作的数据的大小,而是强调这是x86-64指令集的系统。
Ps:%rip是程序计数器PC,%rsp是栈指针。
补充:栈帧保存的是一使用的地址的末尾
1.6.3传递数据
调用过程中,
1、下一级的过程对上一级的过程数据作为参数进行传递
2、调用下一级时,需要对对正在运行的上一级进行保存
下列情况的_局部数据 _只能保存到内存当中;
1.寄存器数量不足
2.对一个局部变量使用地址运算符&,因此必须能够为他产生一个栈空间存储(地址引用)
3.某些局部变量是数组或结构,因此必须为其创建一个数组或结构引用被访问到(地址引用)
1.7指针
1.7.1指针的运用
指针是C语言的的一个核心特色。——它能通过一个地址大小的对不同数据结构中的元素产生引用。
即:
1、每一个指针对应一个类型(可以理解成指针引用的对象是什么)
(指针类型不是机器代码中的一部分,只是C语言提供的一种抽象手段,帮助计算机寻址)
2、每一个指针都有一个值
(非0值的指针指向一个具体的地址,如果是0则说明不指向任何地址)
3、指针通过&创建。
(底层是通过leaq命令实现的,实现指针数据的传送)
4、指针通过*引用对象
(其引用的对象是和下一级的对象类型一致,可能也是一个指针)
5、数组和指针密切相关
(数组名可以作为指针使用)
6、指针可以强转,并且值改变其类型,而不是改变内部储存的数据
7、指针也可以指向_函数_
(前一节的过程介绍了一个底层封装调用函数的方法,实际上在完成数据的准备后,开始执行调用的过程的第一条指令
,这里函数的指针,如int (*fp)(int x,int *p); 实际函数指针fp指向的是这个过程第一条指令。)
补充:函数指针声明 int (*fp)(int*)中的 *fp一定要加括号!!!
接下来将讨论指针运用时候的问题!!
1.7.2缓存区溢出
以gets()函数为例,如图:
echo函数实现主要注意,buf这一变量,分配的8个字节。
gets函数通过调用getchar()函数从标准输入读取一行(这里的一行是以EOF或者回车为结尾)到缓存区。
并且1-7字节的字符串为正常读取,因为字符串结尾需要加入NULL表示结尾。
但是编译器是如何处理这种可能导致的溢出?————接下来观察GCC生成的echo汇编代码
实际上,GCC编译器为8字节的空间分配了24字节的实际值。对于这一系列的内存空间,被分配了不同的内存属性。
利用之前的栈空间的知识:
1、24字节的数据为这个数据结构内存空间。
2、有8个字节是64位系统保存的下一个指令的地址(所以是8个字节)
3、caller保存的状态是上一级寄存器信息、调用的多余参数。
栈溢出的危害:
溢出会造成返回地址的覆盖,从而导致返回地址可以受到外界的攻击代码攻击,ret将会设置成被覆盖的攻击代码的指令!
1.8GDB调试常用指令
1.9浮点数的汇编实现
这里用到的媒体寄存器,操作指令和规则和寄存器相似,暂时不做总结
第四章:处理器体系结构
第五章:优化程序性能
高效的程序的特点:
1、适当的算法
2、合适的数据结构
3、合理运用编译器,使得代码能准确编译成高效可执行的源代码
(本章主要从编译器的角度出发,了解程序的优化)
1优化编译器的能力
编译器能对代码进行优化:
从第三章了解到可以控制优化等级,改变GCC编译器的优化级别,对应的优化级别越高,通常性能更好(推测到一定优化等级会
溢出)
编译器的内联 (inlineing)替换:
对包含函数调用的代码中的函数展开称为函数替换。(这减少了函数调用的开销,也允许了对展开的代码进行进一步优化)
编译器对代码优化的选择:
只进行_安全 _的优化。对于程序所有的情况,编译器不能优化掉任意一种情况,实现的功能与原本的代码功能相同!
以此段代码为例,编译器不知道*P和*Q是否相等,编译器会假定*P和*Q的指针有可能指向同一个位置,所以这就限制了可
能的优化策略。
2程序性能的表示
程序性能的度量单位------>每个元素的周期数(Cycles Per Element,CPE)
>>这是作为改进程序性能的指导方法
观察给出的优化方案psum2和原版代码psum1的时钟周期图,很简单的就能从图中得出性能的优化程度。
——————>这里的斜率就是CPE
2.1消除循环的低效率
循环带来的低效率:
1、冗余相的存在
可以看到lower1每一次执行判断条件时候,编译器不知道strlen(s)的值是否改变,所以每一次都要执行一次strlen操作,C
语言的strlen操作的时间复杂度是O(n),所以导致lower1这个循环时时间复杂度为O(n^2)。
代码lower2则是降低了时间复杂度,使其复杂度变为O(n).
>>时间复杂度是编程时估计程序性能的一种手段,具体需要根据设计的指令类型等等表现成CPE的形式,如下图(下图的斜率就是CPE):
2.2减少过程调用
过程调用将会执行比直接使用指令需要更多的读取、存储的操作,从而造成CPE的降低。
2.3消除不必要的内存引用
如果循环里有内存引用的话,每次进入循环都会对引用的地址进行取值,造成不必要的浪费。
那么为什么*p这种内存别名的方法,和直接用变量名a的方法有区别吗?
1、对内存别名*p等的循环操作,每一次迭代都会对内存别名所指向的内存数据进行检查,这也保证了该内存数据没有被其他程序所改变。
2、但对于变量p的数据的直接操作则不会多次检查,如果循环操作在寄存器足够的状态下,会长期保存在寄存器中,直到循环迭代结束,
数据被写回
2.4指令级并行
1、指令级并行:在某些设计中能实现100或者更多指令的处理
2、描述程序性能的两种下界:
1)延迟界限:如果不同指令级之间均为顺序执行,下一条指令执行前,必须停止前一条,就会遇到延迟界限
2)吞吐量界限:处理器单元的原始最大计算能力(硬件限制)
2.5循环展开
循环展开是一种程序上的变化,它通过增加每次迭代计算的元素的数量,减少循环数量
1)减少了不直接有助于程序结果的数量
2)减少了整个计算中关键路径上的操作数量
Ps:以上均为n*1的展开;此处的n为每次循环的步长,1为累计变量数量;累计变量的数量变多,性能也能得到
优化。
第三章:存储器的层次结构
:0
寄存器中数据访问时钟周期 | L1L2的高速缓存 | 主存 | 磁盘 |
---|---|---|---|
0 | 4~75 | >100 | >10000000 |
DRAM比磁盘快10w倍,SRAM比磁盘快100W倍
1存储器层次中的缓存
缓存又称高速缓存(cache,读作cash)是一个小而快速的存储设备,作为更大更慢设备的缓冲区域。
Ps:缓存不是公用的一个区域!!对于每一层k,位于k层的更快的更小的存储设备提供给k+1层的更大更慢的存储设备的
缓存。
第一次访问数据是将数据从下一层的块往上层传递一个块(第k+1层的存储器被划分为连续的数据对象组块(chunk),又称block)
Ps:DRAM主存时磁盘数据块的缓存,是由操作==系统软件==和CPU上的==地址翻译硬件==共同管理的!!!
1.1缓存的使用
当程序需要对第k+1层对象d进行访问时,首先要对k层的缓存区进行查找,查看是否存在,
如果数据在缓存区存在:缓存命中
如果数据不在缓存区存在:缓存不命中
1.1.1缓存命中
命中的情况下将不会对下一层进行搜索,而是直接对其访问即可,节约了访问下一级存储的时间。
1.1.2缓存不命中
如果缓存不命中,那么第k层存储器将会对含有d的块进行传递,传递到第k层的缓存区,如果缓存区满了,则会覆盖掉一个块
(覆盖的这个过程称为_替换_或者_驱逐_,被驱逐的块叫做牺牲块————如何替换块,则是一个替换策略)。
冷缓存:空的未被使用的缓存,由于冷缓存不命中称为强制不命中或冷不命中
1.1.3缓存的作用
时间局部性:在第一次复制到缓存后,再次用到这个块的次数越多,称时间局部性越好
空间局部性:期望后面对该块中其他对象访问节约的时间能够补偿第一次复制该块的花费