博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

计算机组成原理学习笔记(一)

Posted on 2023-01-30 13:12  steve.z  阅读(270)  评论(0编辑  收藏  举报

一、计算机发展四个阶段

第一阶段:电子管计算机

特点:

集成度小,占用空间大

功耗高,运行速度慢

操作复杂,更换程序需要接线

第二阶段:晶体管计算机

特点:

集成度相对提高,空间占用相对小

功耗相对较低,运行速度较快

操作相对简单,交互更加方便

第三阶段:集成电路计算机

特点:

体积更小

功耗更低

计算速度更快

操作系统雏形: IBM 推出的 System/360

第四阶段:超大规模集成电路计算机

特点:

一个芯片集成了上百万的晶体管

速度更快、体积更小、价格更低、更被大众接受

用途更丰富:文本处理、表格处理、高交互的游戏与应用

第五阶段:未来计算机

1. 生物计算机

2. 量子计算机

二、微型计算机发展史

单核 CPU

多核 CPU

三、计算机分类

1. 超级计算机

特点:

功能强、运算速度快、存储容量大

多用于国家高科技领域和尖端技术研究(天气预报、海洋监测、科学计算、...)

标记运算速度的单位:TFlop/s

1TFlop/s = 每秒一万亿次浮点计算

超级计算机排名:

Summit (IBM)

太湖之光 (中国)

Sierra (IBM)

国内超级计算机排名:

太湖之光

天河二号

天河一号

2. 大型计算机

特点:

大型机、大型主机、主机

性能高、可处理大量数据与复杂运算

3. 迷你计算机(服务器)

特点:

也称为小型机、普通服务器

不需要特殊的空调场所

具备不错的算力,可以完成较复杂的运算

去 IOE 行动:把大型机替换为普通服务器的集群。

4. 工作站

特点:

高端的通用微型计算机,比个人计算机性能更强大

类似于普通台式电脑,体积较大,但性能强劲

5. 微型计算机

特点:

个人计算机

台式机、笔记本电脑、一体机

包含计算机所必备的硬件

四、计算机体系结构

1. 冯诺依曼体系

将程序指令和数据一起存储的计算机设计概念结构。

存储程序指令,设计通用电路

冯诺依曼体系:

必须有存储器

必须有控制器

必须有运算器

必须有输入设备

必须有输出设备

冯诺依曼瓶颈

CPU 和 存储器速率之间的问题

CPU = 运算器 + 控制器

2. 现代计算机结构

现代计算机结构

在冯诺依曼体系结构基础上进行修改

解决 CPU 与 存储设备之间的性能差异问题

CPU = 运算器 + 控制器 + 存储器(寄存器)

五、计算机的层次与编程语言

1. 程序翻译(编译) 与 程序解释

编译:生成可执行文件

解释:不生成可执行文件

2. 计算机的层次与编程语言

虚拟机器

应用软件

应用层

系统软件

高级语言层

汇编语言层

操作系统层

实际机器

传统机器层

微程序机器层

硬件逻辑层

硬件逻辑层

  • 门、触发器等逻辑电路组成
  • 属于电子工程领域

微程序机器层

  • 编程语言是微指令集
  • 微指令所组成的微程序直接交由硬件执行
  • 生产硬件公司程序员编写

传统机器层

  • 编写语言是 CPU 指令集(机器指令)
  • 编程语言直接与硬件相关(例如:Intel 与 AMD 的CPU指令集不同)

微指令、微程序、机器指令

一条机器指令对应一个微程序

一个微程序对应一组微指令

操作系统层

  • 向上提供了简易的操作界面
  • 向下对接了指令系统,管理硬件资源
  • 操作系统是位于软件和硬件之间的适配层

汇编语言层

  • 编程语言是汇编语言
  • 汇编语言可以翻译成可执行的机器语言
  • 汇编器,完成翻译过程

高级语言

  • Python、Java、C++、C、Golang、......

应用层

  • office系列软件等
  • 满足计算机针对某种用途而专门设定

六、计算机的计算单位

1、容量单位

1 byte = 8 bit
1 KB = 1024 byte
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB
1 PB = 1024 TB
1 EB = 1024 PB


1024 = 2^10

1 GB = 1024 * 1024 * 1024 KB
1 GB = 1024 * 1024 * 1024 * 8 bit


2、速度单位

网络速度

100M = 100M/s

网络常用单位为:Mbps

100M/s = 100Mbps = 100Mbit/s
  
100Mbit/s = (100/8)MB/s = 12.5MB/s


CPU 速度

*** CPU 速度:***

  • CPU 速度一般体现为 CPU 的时钟频率
  • CPU 的时钟频率单位一般是 赫兹(Hz)
  • 主流 CPU 的时钟频率都是 2GHz 以上
  • 2GHz = 2*1000^3 Hz = 每秒20亿次,表示 CPU 每秒钟高低电平变换的频率

赫兹Hz

  • 秒分之一,每秒钟的周期性变动重复次数的计量
  • 赫兹并不是描述计算机领域所专有的单位

七、计算机的字符与编码集

1、字符编码集的历史

ASCII 码

1、使用 7 个bit就可以完全表示 ASCII 码

2、包含 95 个可打印字符, 33个不可打印字符(包含控制字符),共计 128 个

Extended ASCII 码

用 8 bit 代替 7 bit,128 个字符扩充为 256 个字符

包含了:

  • 常见的数学运算符
  • 带音标的欧洲字符
  • 其他常用字符、表格符等

字符编码集的国际化

2、中文编码集

中文编码集

一、GB2312 《信息交换用汉字编码集——基本集》

  • 共收录了 7445 个字符(6763个汉字 和 682个其他符号)

二、GBK

  • 《汉字内码扩展规范》
  • 向下兼容 GB2312,向上支持国际 ISO 标准
  • 收录了 21003 个汉字,支持全部中日韩文字

三、Unicode (兼容全球的字符编码集)

  • 统一码、万国码、单一码
  • Unicode定义了世界通用的符号集,UTF-* 实现了编码
  • utf-8 以字节为单位对 Unicode 进行编码

  • Windows 系统默认使用 GBK 编码

  • 编程推荐使用 utf-8 编码


八、计算机总线与 I/O 设备

1、计算机总线 Bus

总线概述

总线概述

解决不同设备间通信问题。

总线分类

  • 片内总线:芯片内部的总线。高集成度芯片内部的信息传输线
    • CPU 内部链接寄存器与寄存器、寄存器与控制器、运算器、高速缓存等
  • 系统总线
    • 链接计算机外围设备的的各种总线(硬盘、IO设备、USB插槽、...
    • 数据总线、地址总线、控制总线

数据总线

  • 双向传输各个部件的数据信息
  • 数据总线的位数(总线宽度)是数据总线的重要参数。一般与 CPU 位数相同(32位、64位)

地址总线

  • 指定源数据或目的数据在内存中的地址
  • 地址总线位数与存储单元有关
  • 地址总线位数=n,寻址范围:0~2^n

控制总线

  • 发出各种控制信号的传输线
  • 控制信号经由控制总线从一个组件发给另外一个组件
  • 控制总线可以监视不同组件之间的状态(就绪/未就绪)

USB : Universal Serial Bus, 通用串行总线。

总线仲裁

1、为什么需要仲裁

解决不同设备使用总线的优先级问题。

通过仲裁控制器实现

2、总线仲裁的方法

① 链式查询方法

好处:

  • 电路复杂度低,仲裁方式简单

坏处:

  • 优先级低的设备难以获得总线使用权
  • 对电路故障敏感
② 计时器定时查询法

仲裁控制器对设备编号并使用计数器累计计数

2、接收到仲裁信号后,往所有设备发出计数值

计数值与设备编号一致则获得总线使用权

③ 独立请求方法

1、每个设备均有总线独立连接仲裁器

2、设备可单独向仲裁器发送请求和接收请求

3、当同时收到多个请求信号,仲裁器有权按优先级分配使用权

4、好处:

  • 响应速度快,优先顺序可动态改变

5、缺点:

  • 设备连线多,总线控制复杂

2、计算机输入/输出设备

1、常见的输入输出设备

字符输入设备

键盘

图形输入设备

鼠标、数位板(手写板)、扫描仪、

图像输出设备

显示器、打印机、投影仪

2、输入输出接口的通用设计

输入输出接口的通用设计

数据线

  • 是IO设备与主机之间进行数据交换的传送线
  • 单向传输数据的数据线
  • 双向传输数据的数据线

状态线

  • 向主机报告IO设备状态的数据线
  • 查询设备是否已经正常连接并就绪
  • 查询设备是否已经被占用

命令线

  • CPU 向设备发送命令的信号线
  • 发送读写信号
  • 发送启动停止信号

设备选择线

  • 主机选择IO设备进行操作的信号线
  • 对连接在总线上的设备进行选择

3、CPU 与 IO 设备的通信方法

1. 程序中断法

1、当外围IO设备就绪时,向 CPU 发出中断信号

2、CPU 有专门的电路响应中断信号

程序中断法:


提供低速设备通知 CPU 的一种异步的方式

CPU 可以高速运转同时兼顾低速设备的响应


2. DMA(直接存储器访问)

1、DMA 直接连接主存与IO设备

2、DMA 工作时不需要 CPU 参与


CPU ←→ 主存 ←→ DMA ←→ IO设备

当主存与IO设备交换信息时,不需要中断 CPU


九、计算机存储器

1、计算机存储器概览

存储器的分类

一、存储介质分类:

① 半导体存储器:内存、U盘、固态硬盘

② 磁存储器:磁带、磁盘

二、存取方式分类:

① RAM(随机存储器):随机读取、与位置无关。

② 串行存储器:与位置有关、按顺序查找。

③ ROM(只读存储器):只读不写。

存储器的层次结构


1、购买存储器时考虑因素:读写速度、存储容量、价格。

2、容量 + 价格 → 位价:每比特位价格。


存储器层次结构:

1、缓存:速度最快,价格最高。

2、主存:速度适中,价格适中。

3、辅存:速度慢,价格低。

Snip20230127_1

缓存-主存层次:

1、原理:局部性原理

2、实现:在 CPU 与主存之间增加一层速度快(容量小)的 Cache

3、目的:解决主存速度不足的问题。

局部性原理:

CPU 访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。

时间局部性、空间局部性

主存-辅存层次:

1、原理:局部性原理

2、实现:主存之外增加辅助存储器(磁盘、SD卡、U盘等)

3、目的:解决主存容量不足问题

2、计算机的主存储器与辅助存储器


主存储器:内存。

辅助存储器:磁盘。


主存储器(内存):

一、基本概念:

1、RAM:Random Access Memory

2、RAM 通过电容存储数据,必须每隔一段时间刷新一次

3、如果断电,那么一段时间后将丢失所有数据

二、结构:

半导体存储体、读写电路、控制电路、驱动器、译码器

Snip20230127_2

32位系统:最大支持 4GB内存(因为地址总线最多32位)

64位系统:最大支持 234 GB内存

辅助存储器(磁盘)

1、术语:磁道、扇区、磁头、磁头位置、磁头方向

2、磁盘表面是可磁化的硬磁特性材料

3、移动磁头径向运动读取磁道信息

磁盘调度算法:

1、先来先服务算法

按顺序访问进程的磁道读写需求

2、最短寻道时间算法

优先访问离磁头最近的磁道

3、扫描算法(电梯算法)

每次只往一个方向移动(由内向外或者由内向外)

到达一个方向需要服务的尽头再反方向移动

4、循环扫描算法

基于扫描算法,但只由一个方向读取(从内向外或从外向内,读取多次)

3、计算机的高速缓存

高速缓存目的:解决 CPU 与主存的速度不匹配问题。

一、高速缓存的工作原理

字:

是指存放在一个存储单元中的二进制代码组合。

字块:

存储在连续存储单元中而被看做是一个单元的一组字。

Snip20230130_1

字的地址包含两个部分:

1、前 m 位指定字块的地址

2、后 b 位指定字在字块中的地址

Snip20230130_2

例:

Snip20230130_3

Snip20230130_4

高速缓存的工作原理指标:

1、命中率

2、访问效率

命中率:

1、命中率是衡量缓存的重要性能指标

2、理论上 CPU 每次从高速缓存取数据都能取到时,命中率为 1

3、命中率计算:

Snip20230130_5

访问效率:

Snip20230130_6

命中率、访问效率、平均访问时间计算案例:

Snip20230130_7

Snip20230130_9

二、高速缓存的替换策略

高速缓存替换策略

1、随机算法
2、先进先出算法(FIFO)

1、将高速缓存看做是一个先进先出的队列

2、优先替换最先进入队列的字块

3、最不经常使用算法(LFU)

1、优先淘汰最不经常使用的字块

2、需要额外的空间记录字块的使用频率

4、最近最少使用算法(LRU)

1、优先淘汰一段时间内没有使用的字块

2、有多种实现方法,一般使用双向链表实现

3、把当前访问节点置于链表前面(保证链表头部节点是最近使用的)

十、计算机指令系统

1、机器指令形式

机器指令形式:

1、机器指令主要由两部分组成:操作码、地址码

2、操作码:

① 指明指令所要完成的操作

② 操作码的位数反映了机器的操作种类(比如 8 位,最多有 28 = 256 种操作)

3、地址吗:

① 地址码直接给出操作数或者操作数的地址

② 分三地址指令、二地址指令 和 一地址指令

三地址指令: 操作码(OP)、地址1、地址2、地址3。

(addr1) OP (addr2) → (addr3)

Snip20230127_3

二地址指令: 操作码(OP) 、地址1、地址2

(addr1) OP (addr2) → (addr1) 或 (addr2),即将结果放入 addr1 或 addr2 中

Snip20230127_4

一地址指令: 操作码(OP)、地址1

Snip20230127_5

零地址指令(无地址指令):

在机器指令中无地址码

空操作、停机操作、中断返回操作等

2、机器指令的操作类型

机器指令的操作类型:

1、数据传输类型:

① 可以发生在寄存器之间、寄存器与存储单元、存储单元之间传送

② 数据读写、交换地址数据、清零置一等操作

2、算数逻辑操作类型

操作数之间的加减乘除运算

操作数的与、或、非等逻辑位运算

3、移位操作

数据左移(相当于乘以2)、数据右移(相当于除以2)

完成数据在算数逻辑单元的必要操作

4、控制指令

等待指令、停机指令、空操作指令、中断指令等

3、机器指令的寻址方式

一、指令寻址

1、顺序寻址

2、跳跃寻址

二、数据寻址

1、立即寻址

指令中包含操作数

运行时无需访问存储器

优点:

速度快

缺点:

地址码位数限制操作数表示范围

2、直接寻址

机器指令中包含操作数在主存的地址

寻找操作数简单,无需计算数据地址

优点:

寻找操作数简单

缺点:

地址码位数限制操作数寻址范围

3、间接寻址

指令地址码给出的是操作数地址的地址

优点:

操作数寻址范围大

缺点:

速度较慢

十一、计算机的控制器

控制器是协调和控制计算机运行的


Snip20230127_6

程序计数器

  • 程序计数器用来存储下一条指令的地址
  • 循环从程序计数器中拿出指令
  • 当指令被拿出时,指向下一条指令

时序发生器

  • 电气工程领域,用于发送时序脉冲
  • CPU 依据不同的时序脉冲有节奏的进行工作(CPU 的 “节拍器”)

指令译码器

  • 指令译码器是控制器的主要部件之一
  • 计算机指令由操作码和地址码组成
  • 翻译操作码对应的操作以及控制传输地址码对应的数据

指令寄存器

  • 指令寄存器也是控制器的主要部件之一
  • 从主存或高速缓存取计算机指令

主存地址寄存器

  • 保存当前 CPU 正要访问的内存单元的地址

主存数据寄存器

  • 保存当前 CPU正要读或写的主存数据

通用寄存器

  • 用于暂时存放或传送数据或指令
  • 可保存 ALU 的运算中间结果
  • 容量比一般专用寄存器要大

十二、计算机的运算器

运算器是用来进行数据运算加工的

Snip20230127_7

数据缓冲器

1、输入缓冲

暂时存放外设送来的数据

2、输出缓冲

暂时存放送往外设的数据

ALU (算数逻辑单元)

  • 运算器的主要组成
  • 常见的位运算(左右移、与或非)
  • 算术运算(加减乘除等)

Snip20230127_8

通用寄存器

  • 用于暂时存放或传送数据或指令
  • 可保存 ALU 的运算中间结果
  • 容量比一般专用寄存器要大

状态字寄存器

  • 存放运算状态(条件码、进位、溢出、结果正负等)
  • 存放运算控制信息(调试跟踪标记位、允许中断位)

十三、计算机指令的执行过程

1、指令执行过程

1、取指令

从缓存取指令

送到指令寄存器

2、 分析指令

指令译码器译码

发出控制信号

程序计数器 +1

3、执行指令

装载数据到寄存器

ALU 处理数据

记录运算状态

送出运算结果

取指令、分析指令,由控制器负责;执行指令由运算器负责。二者不能同时工作,导致CPU综合利用率不高。

2、CPU 的流水线设计

CPU 流水线设计:

(串行,非流水线模式)取指令→分析指令→执行指令→ 取指令→分析指令→执行指令......

下图为 CPU 流水线模式演示:

Snip20230127_9

Snip20230127_10

Snip20230127_11

十四、进制运算基础知识

1、进制运算基础

*** 进制概述***

1、进制定义:

① 进位制是一种计数方式,亦称进位计数法或位值记数法

② 有限种数字符号来表示无限的数值

③ 使用的数字符号的数目称为这种进位制的基数或底数

2、常见的进制:

十进制、八进制(0o...)、十六进制(0x....)、二十进制、二进制...(0b)、六十进制

二进制运算的基础

1、二进制转十进制:按权展开法。

2、(整数)十进制转二进制:重复相除法。(重复除以2,得商取余数,将余数倒着拼接起来。)

3、(小数)二进制转十进制:按权展开法。

(0.11001)从小数点右边第一位开始。1 * 2-1 + 1 * 2-2 + 0 * 2-3 + 0 * 2-4 + 1 * 2-5

4、(小数)十进制转二进制:重复相乘法。(重复乘以2,得积,取1或0,从上到下将1和0拼起来)

Snip20230127_12

十五、二进制数据的表示方法

1、有符号数与无符号数

计算机表示正负数,使用 原码表示法(0 表示正数,1表示负数)

原码表示法:

1、使用 0 表示正数、1 表示负数

2、规定符号位位于数值第一位

3、表达简单明了,是人类最容易理解的表示法

4、原码表示法存在的问题:

① 按照原码表示法,0 可以有两种表示:00(+0)、10(-0),存在歧义。

② 原码进行运算非常复杂,特别是两个操作数符号不同的时候。

1> 判断两个操作数绝对值大小

2> 使用绝对值大的数减去绝对值小的数

3> 对于符号值,以绝对值大的数为准。

Snip20230128_2

2、二进制的补码表示

补码定义:

Snip20230128_3

3、二进制的反码表示

使用反码目的:

找出原码和补码之间的规律,消除转换过程中的减法。

反码定义:

1、大于等于 0 时,反码等于原码

2、小于 0 时,反码等于


1、正数的原码、补码、反码都一样。

2、负数的反码等于:原码除符号位以外,按位取反。

3、负数的补码等于:反码 + 1。


Snip20230128_1

4、小数的二进制补码表示

规则同正数

十六、二进制数据的运算

1、定点数与浮点数

1、定点数的表示方法:

定点数概念:

小数点固定在某个位置的数,称之为定点数。

定点数的两种表示方法:

1、小数点在符号位与数值为之间。(纯小数)

2、小数点在数值位最后。(纯整数)

对于非纯小数 or 非纯整数需要乘以比例因子以满足定点数的保存格式

Snip20230129_1

2、浮点数表示方法

① 浮点数的表示格式

科学计数法:

Snip20230128_5

Snip20230128_7

尾数必须使用纯小数

Snip20230128_9

② 浮点数的表示范围

Snip20230129_2

Snip20230129_4

浮点数表示范围

1、单精度浮点数(float):使用 4 字节,32位来表达浮点数

2、双精度浮点数:(double):使用 8 字节,64位来表达浮点数

③ 浮点数的规格化

1、尾数规定使用纯小数

2、尾数最高位必须是 1

Snip20230129_5

案例:

Snip20230129_6

Snip20230129_7

3、定点数与浮点数对比

1、当定点数与浮点数位数相同时,浮点数表示的范围更大

2、当浮点数尾数为规格化数时,浮点数的精度更高

3、浮点数运算包含阶码和尾数,浮点数的运算更为复杂

4、浮点数在数的表示范围、精度、溢出处理、编程等方面均优于定点数

5、浮点数在数的运算规则、运算速度、硬件成本方面不如定点数

2、定点数的加减法运算

① 定点数的加法运算

Snip20230129_8

Snip20230129_9

Snip20230129_10

Snip20230129_13

判断溢出

双符号位判断法

1、原来表示正数的符号位 0,用 00 表示。原来表示负数的符号位 1,用 11 表示。

2、双符号位产生的进位丢弃掉。

3、结果的双符号位不同则表示溢出。

Snip20230129_14

Snip20230129_15

② 定点数的减法运算(转换为加法操作)

Snip20230129_17

Snip20230129_18

3、浮点数的加减法运算

浮点数加减法计算步骤:

1、对阶操作

使两个浮点数的阶码一致,使得尾数可以进行运算

Snip20230130_10

2、尾数求和

1、使用补码进行运算

2、减法运算转换为加法运算

Snip20230130_11

3、尾数规格化

对补码进行规格化需要判断两种情况:尾数 > 0 和 尾数 < 0

Snip20230130_14

4、舍入

1、"0 舍 1 入" 法(二进制的四舍五入)

Snip20230130_15

5、溢出判断

1、定点运算双符号位不一致为溢出,浮点运算尾数双符号位不一致不算溢出。

2、浮点运算主要通过阶码的双符号位判断是否溢出。如果规格化后,阶码双符号位不一致,则认为是溢出。

Snip20230130_16
Snip20230130_17
Snip20230130_18
Snip20230130_19

Snip20230130_21

4、浮点数的乘除法运算

浮点数乘法运算法则:

阶码相加,尾数求积

Snip20230130_22

浮点数除法运算法则:

阶码相减,尾数求商

Snip20230130_25

案例:

Snip20230130_26

十七、实现双向链表

1、双向链表的原理与实践

Snip20230128_10

双向链表的优点:

1、可以快速找到一个节点的下一个节点

2、可以快速找到一个节点的上一个节点

3、可以快速去掉链表中的某一个节点

# 双向链表 Python demo

#
#  doubly_linked_list.py
#  pythonProject 
#
#  Created by Z. Steve on 2023/1/28.
#


# 实现一个链表节点类
class Node:
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

    def __str__(self):
        val = '{%d: %d}' % (self.key, self.value)
        return val

    def __repr__(self):
        val = '{%d: %d}' % (self.key, self.value)
        return val


# 实现一个双向链表类
class DoublyLinkedList:
    # 构造函数
    def __init__(self, capacity=0xffff):
        self.capacity = capacity

        # 头部节点引用
        self.head = None

        # 尾部节点引用
        self.tail = None

        # 保存当前链表所存储的节点数, 默认值是 0
        self.size = 0

    # 从头部添加节点
    def __add_head(self, node):
        # 如果 self.head 为空,则表示链表中没有节点
        if not self.head:
            # 设置当前链表的头部、尾部节点均为 node
            self.head = node
            self.tail = node
            # 设置链表的 head 节点的上一个和下一个节点为 None
            self.head.prev = None
            self.head.next = None
        else:
            # 执行 else 表示当前链表的 head 节点不为 空
            node.next = self.head
            self.head.prev = node
            self.head = node
            self.head.prev = None

        # 添加节点结束后,让链表 size + 1,同时返回这个节点
        self.size += 1
        return node

    # 从尾部添加节点
    def __add_tail(self, node):
        if not self.tail:
            self.head = node
            self.tail = node
            self.tail.prev = None
            self.tail.next = None
        else:
            node.prev = self.tail
            node.next = None
            self.tail.next = node
            self.tail = node

        self.size -= 1
        return node

    # 删除第一个节点 head
    def __rm_head(self):
        # 如果头节点为 None,则直接 return
        if not self.head:
            return
        # 将要删除的 head 节点 赋值给 node
        node = self.head
        if node.next:
            self.head = node.next
            node.next.prev = None
        else:
            self.head = self.tail = None
        self.size -= 1
        return node

    # 删除最后一个节点 tail
    def __rm_tail(self):
        if not self.tail:
            return
        # 将要删除的节点获取到 node 中
        node = self.tail
        if node.prev:
            self.tail = node.prev
            self.tail.next = None
        else:
            self.tail = self.head = None
        self.size -= 1
        return node

    # 删除给定的任意一个节点
    def __remove(self, node):
        # 判断如果 node == None 的话,则将 tail 节点赋值给 node 进行删除
        if not node:
            node = self.tail

        if node == self.tail:
            self.__rm_tail()
        elif node == self.head:
            self.__rm_head()
        else:
            node.prev.next = node.next
            node.next.prev = node.prev
            self.size -= 1
        return node

    # public: 弹出 head 节点
    def pop(self):
        return self.__rm_head()

    # public: 添加节点
    def append(self, node):
        return self.__add_tail(node)

    # public: 向头部添加节点
    def insert_at_beginning(self, node):
        return self.__add_head(node)

    # public: 删除节点
    def remove(self, node=None):
        return self.__remove(node)

    # public: 打印链表所有节点
    def print(self):
        p = self.head
        line = ''
        while p:
            line += '%s' % p
            p = p.next
            if p:
                line += '=>'
        print(line)


# 测试双向链表

if __name__ == '__main__':

    # 初始化一个双向列表对象
    dllist = DoublyLinkedList(10)

    # 向 nodes 中添加 10 个节点
    nodes = []
    for i in range(10):
        node = Node(i, i)
        nodes.append(node)

    # 对双向链表对象 dllist 操作
    dllist.append(nodes[0])
    dllist.print()

    dllist.append(nodes[1])
    dllist.print()

    dllist.pop()
    dllist.print()

    dllist.append(nodes[2])
    dllist.print()

    dllist.insert_at_beginning(nodes[3])
    dllist.print()

    dllist.append(nodes[4])
    dllist.print()

    dllist.remove(nodes[2])
    dllist.print()

    dllist.remove()
    dllist.print()

'''
输出结果:
{0: 0}
{0: 0}=>{1: 1}
{1: 1}
{1: 1}=>{2: 2}
{3: 3}=>{1: 1}=>{2: 2}
{3: 3}=>{1: 1}=>{2: 2}=>{4: 4}
{3: 3}=>{1: 1}=>{4: 4}
{3: 3}=>{1: 1}
'''

十八、实现置换算法

1、先进先出算法(FIFO, First in First out)

  • 把告诉缓存看做是一个先进先出的队列

  • 优先替换最先进入队列的字块

# FIFO demo

#
#  fifo_cache.py
#  pythonProject 
#
#  Created by Z. Steve on 2023/1/28.
#

from doubly_linked_list import DoublyLinkedList, Node


class FIFOCache(object):
    def __init__(self, capacity):
        self.capacity = capacity
        self.size = 0
        # FIFOCache 对象中维护的一个键值对列表
        self.map = {}
        self.list = DoublyLinkedList(self.capacity)

    def get(self, key):
        if key not in self.map:
            return -1
        else:
            node = self.map.get(key)
            return node.value

    def put(self, key, value):
        if self.capacity == 0:
            return
        if key in self.map:
            node = self.map.get(key)
            self.list.remove(node)
            node.value = value
            self.list.append(node)
        else:
            # 如果 FIFOCache 已经满了,则删除一个节点
            if self.size == self.capacity:
                node = self.list.pop()
                del self.map[node.key]
                self.size -= 1

            # 创建一个 Node 对象
            node = Node(key, value)
            # 将该节点添加到双向列表尾部
            self.list.append(node)
            self.map[key] = node
            self.size += 1

    def print(self):
        self.list.print()


# 测试 FIFOCache

if __name__ == '__main__':
    cache = FIFOCache(2)
    cache.put(1, 100)
    cache.print()

    cache.put(2, 200)
    cache.print()

    print(cache.get(1))

    cache.put(3, 300)
    cache.print()
'''
输出结果:

{1: 100}
{1: 100}=>{2: 200}
100
{2: 200}=>{3: 300}
'''

2、最近最少使用算法(LRU, Least Recently Used)

  • 优先淘汰一段时间内没有使用的字块
  • 有多种实现方法, 一般使用双向链表
  • 把当前访问节点置于链表前面(保证链表头部节点是最近使用的)
# LRU demo

#
#  lru_cache.py
#  pythonProject 
#
#  Created by Z. Steve on 2023/1/28.
#

from doubly_linked_list import DoublyLinkedList, Node


class LRUCache(object):
    def __init__(self, capacity):
        self.capacity = capacity
        self.map = {}
        self.list = DoublyLinkedList(self.capacity)

    def get(self, key):
        if key in self.map:
            # 将该节点取出
            node = self.map[key]
            # 删除原位置节点
            self.list.remove(node)
            # 将该节点添加到首位置
            self.list.insert_at_beginning(node)
            # 返回该节点的值
            return node.value
        else:
            # 如果 key 在 map 中不存在,则返回 -1
            return -1

    def put(self, key, value):
        # 判断 key 是否已经在缓存中了
        if key in self.map:
            old_node = self.map.get(key)
            self.list.remove(old_node)
            old_node.value = value
            self.list.insert_at_beginning(old_node)
        else:
            # 创建一个新节点
            node = Node(key, value)
            # 判断当前链表 size 是否已满,如果满了则删除最后一个
            if self.list.size >= self.list.capacity:
                # 链表已满
                # 1. 删除尾部节点, remove() 函数不传参数表示删除最后一个节点
                old_node = self.list.remove()
                # 2. 在 map 映射中也删除该节点
                self.map.pop(old_node.key)

            # 添加新节点
            self.list.insert_at_beginning(node)
            # map 中保存新的映射关系
            self.map[key] = node

    def print(self):
        self.list.print()


# 测试
if __name__ == '__main__':
    # 1. 创建一个 LRUCache
    cache = LRUCache(2)
    # 2. 操作 cache
    cache.put(1, 100)
    cache.print()

    cache.put(2, 200)
    cache.print()

    # (1, 100) 会被淘汰
    cache.put(5, 500)
    cache.print()

    print(cache.get(2))
    
    # (1, 100) 已经被淘汰
    print(cache.get(1))

'''
输出结果:

{1: 100}
{2: 200}=>{1: 100}
{5: 500}=>{2: 200}
200
-1
'''

3、最不经常使用算法(LFU, Least Frequently Used)

  • 优先淘汰最不经常使用的字块
  • 需要额外的空间记录字块的使用频率

在多个使用频率同样低的字块中(把相同频率的节点连城一个链表),再用 FIFO 算法进行淘汰。

# LFUCache demo


#
#  lfu_cache.py
#  pythonProject 
#
#  Created by Z. Steve on 2023/1/28.
#

from doubly_linked_list import DoublyLinkedList, Node


# 带有记录节点使用频率的 Node 类
class LFUNode(Node):
    def __init__(self, key, value):
        # 记录节点使用频率
        self.freq = 0
        # 调用父类构造函数
        super(LFUNode, self).__init__(key, value)


class LFUCache(object):

    # 构造函数
    def __init__(self, capacity):
        self.capacity = capacity
        self.map = {}
        self.size = 0

        # 保存不同使用频率对应的双向链表
        # key: 节点使用频率; value: 双向链表
        self.freq_map = {}

    # 更新节点使用频率
    def __update_node_freq(self, node):
        # 1. 获取节点 node 的使用频率
        freq = node.freq

        # 2. 删除(如果对应的频率在 freq_map 中的双向链表 size 为 0,则删除这个键值对)
        node = self.freq_map[freq].remove(node)
        if self.freq_map[freq].size == 0:
            del self.freq_map[freq]

        # 3. 更新频率
        freq += 1
        node.freq = freq
        if freq not in self.freq_map:
            self.freq_map[freq] = DoublyLinkedList()
        self.freq_map[freq].append(node)

    def get(self, key):
        if key not in self.map:
            return -1
        # 根据 key 获得该节点
        node = self.map.get(key)
        # 更新节点访问频率
        self.__update_node_freq(node)
        # 返回该节点的值
        return node.value

    def put(self, key, value):
        if self.capacity == 0:
            return

        if key in self.map:
            # 缓存命中
            # 1. 从缓存中取数据
            node = self.map.get(key)
            # 2. 更新节点的value
            node.value = value
            # 3. 更新节点访问频率
            self.__update_node_freq(node)
        else:
            # 缓存未命中
            # 1. 检查是否满了
            if self.size == self.capacity:
                # 找到使用频率最少的节点删除
                min_freq = min(self.freq_map)
                # 从使用频率最少的 list 中删除一个
                node = self.freq_map[min_freq].pop()
                # 从 self.map 中删除这个 node
                del self.map[node.key]
                self.size -= 1

            # 创建 LFUNode 节点
            node = LFUNode(key, value)
            # 设置该节点访问频率
            node.freq = 1
            # 将该节点加到 map 和 freq_map 中
            self.map[node.key] = node
            if node.freq not in self.freq_map:
                # 新建一个链表
                self.freq_map[node.freq] = DoublyLinkedList()
            # 将新节点加到 freq_map 中
            node = self.freq_map[node.freq].append(node)
            self.size += 1

    def print(self):
        # 打印 self.freq_map
        print('***********************************************')
        for k, v in self.freq_map.items():
            print('freq = %d' % k)
            self.freq_map[k].print()
        print('***********************************************')
        print()
        print()


# 测试
if __name__ == '__main__':
    # 1. 创建一个 LFUCache 对象
    cache = LFUCache(4)

    # 2. 对 cache 进行操作
    cache.put(1, 100)
    cache.print()

    cache.put(2, 200)
    cache.print()

    print(cache.get(1))
    cache.print()

    cache.put(3, 300)
    cache.print()

    print(cache.get(2))
    cache.print()

    print(cache.get(3))
    cache.print()

    cache.put(4, 400)
    cache.print()

    print(cache.get(1))
    cache.print()

    print(cache.get(3))
    cache.print()

    print(cache.get(4))
    cache.print()


'''
输出结果:

***********************************************
freq = 1
{1: 100}
***********************************************


***********************************************
freq = 1
{1: 100}=>{2: 200}
***********************************************


100
***********************************************
freq = 1
{2: 200}
freq = 2
{1: 100}
***********************************************


***********************************************
freq = 1
{2: 200}=>{3: 300}
freq = 2
{1: 100}
***********************************************


200
***********************************************
freq = 1
{3: 300}
freq = 2
{1: 100}=>{2: 200}
***********************************************


300
***********************************************
freq = 1

freq = 2
{1: 100}=>{2: 200}=>{3: 300}
***********************************************


***********************************************
freq = 1
{4: 400}
freq = 2
{1: 100}=>{2: 200}=>{3: 300}
***********************************************


100
***********************************************
freq = 1
{4: 400}
freq = 2
{2: 200}=>{3: 300}
freq = 3
{1: 100}
***********************************************


300
***********************************************
freq = 1
{4: 400}
freq = 2
{2: 200}
freq = 3
{1: 100}=>{3: 300}
***********************************************


400
***********************************************
freq = 1

freq = 2
{2: 200}=>{4: 400}
freq = 3
{1: 100}=>{3: 300}
***********************************************

'''