Author: XFFer_
入门篇
01 冯·诺依曼体系结构:计算机组成的金字塔
02 给你一张只是地图,计算机组成原理应该这么学
03 通过你的CPU主频,我们来谈谈“性能”究竟是什么
04 穿越功耗墙,我们该从哪些方面提升“性能”
原理篇:指令和运算
05 计算机指令:让我们试试用纸带编程
06 指令跳转:原来if…else就是goto
07 函数调用:为什么会发生stack overflow?
08 ELF和静态链接:为什么程序无法同时在LINUX和WINDOWS下运行
09 程序装载:“640K内存”真的不够用么?
10 动态链接:程序内部的“共享单车”
11 二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫?”
12 理解电路:从电报机到门电路,我们如何做到“千里传信”
13 加法器:如何像搭乐高一样搭电路?
14 乘法器:如何像搭乐高一样搭电路?
15 浮点数和定点数:怎么用有限的Bit表示尽可能多的信息?
原理篇:处理器
16 建立数据通路:指令+运算=CPU
17 面向流水线的指令设计:一心多用的CPU
18 冒险和预测(一):hazard是“危”也是“机”
19 冒险和预测(二):流水线里的接力赛
20 冒险和预测(三):CPU里的“线程池”
21 冒险和预测(四):今天下雨了,明天还会下雨吗
22 Superscalar和VLIW:如何让CPU吞吐率超过1
23 SIMD:如何加速矩阵乘法?
24 异常和中断:程序出错了怎么办?
25 CISC和RISC:为什么手机芯片都是ARM
26 GPU
27 FPGA和ASIC:计算机体系结构的黄金时代
28 解读TPU:设计和拆解一块ASIC芯片
29 理解虚拟机:你在云上拿到的计算机是什么样的?
原理篇:储存与I/O系统
30 存储器层次结构全景:数据存储的大金字塔长什么样?
31 局部性原理:数据库性能跟不上,加个缓存就好了?
32 高速缓存:“4毫秒”究竟值多少钱&你确定你的数据更新了吗?
33 MESI协议:如何让多核CPU的高速缓存保持一致?
34 理解内存
35 总线:计算机内部的高速公路
36 输入输出设备:我们并不是只能用灯泡显示0和1
37 理解IO_WAIT:I/O性能到底是怎么回事儿
38 机械硬盘:Google早期用过的“黑科技”
39 SSD硬盘
40 DMA:为什么Kafka这么快?
41 数据完整性
42 分布式计算
*应用篇
43 设计大型DMP系统
44 理解Disruptor
CPU
内存
主板
主板
I/O设备
CPU
算数逻辑单元(Arithmetic Logic Unit, ALU) 和 ***处理器寄存器(Processor Register)***的处理器单元(Processing Unit),用来完成各种算数和逻辑运算,也叫做 运算器。
指令寄存器(Instruction Register) 和 ***程序计数器(Program Counter)***的 控制器单元(Control Unit/CU),用来控制程序的流程,通常就是不同条件下的分支和跳转。
存储器
输出输出设备
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OdvoUsk6-1582080069645)(https://i.loli.net/2019/12/31/eQnvdlwmsrjVY92.jpg)]
计算机的基本组成
运算器、控制器、存储器、输入和输出设备
性能和功耗
计算机的指令
程序如何通过编译器和汇编器变成机器指令的编译过程
操作系统如何链接、装载、执行这些程序
执行的控制过程是由控制器控制的
计算机的计算
二进制和编码-数据在计算机中的表示
数字电路-实现基本运算功能***算术逻辑单元ALU***
CPU的设计
CPU时钟
- 寄存器&内存的锁存器和触发器
数据通路
存储器的原理
响应时间(执行时间)
吞吐率(带宽)
SPEC(Standard Performance Evaluation Corporation)各大CPU和服务器厂商组织的专门用来制定各种“跑分”规则的机构。
Linux下有一个time
的命令,可以统计出Wall Clock Time
下,程序实际在CPU上花了多少时间。
$ time seq 1000000 | wc -l
1000000
real 0m0.101s
user 0m0.031s
sys 0m0.016s
|
是管道符;wc
统计指定文件中的字节数、字数、行数,并显示输出;wc``-c
统计字节数;-l
统计行数;-m
统计字符数;-w
统计字数。
time
命令返回三个值:
real time
即Wall Clock Time,也就是运行程序整个过程中流逝的时间
user time
也就是CPU在运行你的程序,在用户态运行指令的时间
sys time
是CPU在运行你的程序,在操作系统内核里运行指令的时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sw3EZaN7-1582080069647)(https://i.loli.net/2020/01/03/LYWcSk4dzI7eQT8.jpg)]
例:Inter Core-i7-7700HQ 2.8GHz 中 2.8GHz就是电脑的主频
理解为:CPU的一个晶体振荡器(Oscillator Crystal),简称为晶振 能够识别出来的最小的时间间隔。2.8GHz理解为时钟周期时间为1/2.8G。
提升性能有两种选择:
一是提升主频缩短时钟周期时间(更取决于硬件);
二是减少CPU时钟周期数。
对于第二种方法可以对时钟周期数再做一次分解:
指令数 * 每条指令的平均时钟周期数(Cycles Per Instruction,简称CPI) = CPU时钟周期数
平均时钟周期数通过流水线技术让一条指令需要的CPU Cycle尽可能的减少。
CPU的性能 执行效率 取决于:
CPU主频,即计算机硬件,主频越大,执行效率越高;
指令数,完成一个任务所需指令条数越少,执行效率越高;
CPI,单个指令所需的CPU Cycle数,CPI越少,执行效率越高。
把自己想象成一个CPU,计算机主频比做打字速度,打字越快,自然可以多写一些程序;CPI相当于快捷键,熟悉快捷键,在打同样内容时所花费的时间更少;指令数相当于设计的代码是否合理,代码行数越少,自然效率越高。
CPU 一般叫做超大规模集成电路即(Very-Large-Scale Integration,VLSI)
提升CPU计算速度:
增加晶体管密度,提升“制程”,即将CPU芯片做的更小
提升主频
随之而来的是耗电和散热的问题。
功耗 ~= 1/2 * 负载电容 * 电压的平方 * 开关频率 * 晶体管数量
公式中可以看出可以通过降低电压来降低功耗。
当提升主频“难”实现性能提升时,就出现了多核CPU,来提升“吞吐率”,而不是“响应时间”。
阿姆达尔定律
优化后的执行时间 = 受优化影响的执行时间 / 加速倍数 + 不受影响的执行时间
机器码(0/1)和计算机指令的关系
CPU Central Processing Unit
不同的CPU(Intel/ARM/…)能够听懂的机器语言不同,各自支持的语言叫做计算机指令集(Instruction Set)
程序 =(编译)=> 汇编语言(ASM,Assembly Language) =(汇编器)=> 机器语言(十六进制数字,实质上是二进制码,十六进制是为了显示方便)
程序
// test.c
int main()
{
int a = 1;
int b = 2;
a = a + b;
}
Linux中使用gcc
和objdump
打印出汇编代码和机器码
$ gcc -g -c test.c
$ objdump -d -M intel -S test.o
//结果
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
int main()
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int a = 1;
4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
int b =2;
b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
a = a + b;
12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
15: 01 45 fc add DWORD PTR [rbp-0x4],eax
}
18: 5d pop rbp
19: c3 ret
机器码和汇编代码是一一对应的
算数类指令 加减乘除…
数据传输类指令 赋值、在内存里读写数据…
逻辑类指令 与或非…
条件分支类指令 if/else…
无条件跳转指令 调用函数…
MIPS的指令是一个32位的整数,高6位叫操作码(Opcode)
R指令用来算数和逻辑操作,里面有读取和写入数据的寄存器的地址。
I指令用在数据传输、条件分支。
J指令跳转指令,高6位之外的26位都是跳转后的地址。
加法算数指令
add $t0,$s2,$s1
二进制表示
000000 10001 10010 01000 00000 100000
拆分成四个一组
四位二进制数所能表示的最大值也就是一位十六进制数所能表示的最大值
0000 0010 0011 0010 0100 0000 0010 0000
十六进制表示
(0X)02324020
这也是计算机识别的机器语言
CPU就是由一堆寄存器组成的,而寄存器由多个==触发器(Flip-Flop)或者锁存器(Latches)==组成的简单电路
PC寄存器(Program Counter Regiser)也叫指令地址寄存器(Instruction Address Register),用来存放下一条需要执行的计算机指令的内存地址。
指令寄存器(Instruction Register)用于存放当前正在执行的指令。
条件码寄存器(Status Register)用标记位(Flag),存放CPU进行算数或者逻辑计算的结果。
一个程序执行时,CPU根据PC寄存器里的地址,从内存中把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,读取下一条指令。在内存中一个程序的一条条指令是连续保存的,也会连续加载
//test.c
#include
#include
int main()
{
srand(time(NULL));
int r = rand() % 2; //定义整型r是一个随机整数与2相除后的余数0/1
int a = 10;
if (r == 0)
{
a = 1;
} else {
a = 2;
}
硬核编译后
$ gcc -g -c test.c
$ objdump -d -M intel -S test.o
结果
if (r == 0)
3b: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0
3f: 75 09 jne 4a <main+0x4a>
{
a = 1;
41: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
48: eb 07 jmp 51 <main+0x51>
}
else
{
a = 2;
4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
51: b8 00 00 00 00 mov eax,0x0
}
够硬核不,其实把我自己这个非计算机科班的看傻了
解读
r == 0
的条件判断被拆分成了cmp
和jne
两个指令cmp
指令比较前后两个操作数的值,DWORD PTR
代表操作的数据类型是32位的整数,第一个操作数是从[rbp-0x4]
这个寄存器地址拿到的变量r的值,与0x0
即0的十六进制表示比较,结果存入条件码寄存器
如果比较结果为True
,就把零标志条件码(ZF, Zero Flag)设置为1。除此之外还有进位标志(CF, Carry Flag)、符号标志(SF, Sign Flag)、溢出标志(OF, Oveflow Flag)
然后PC寄存器自增,开始执行jne
的指令
jne
是jump if not equal,会查看零标志位,如果是 0,会跳转到跟着操作数4a
的位置。例子中跳转到了else
的语句下
地址51
作用是一个占位符,mov eax, 0x0
实际上给main函数生成了一个默认的为0的返回值到累加器里面
我猜还是很迷
goto
函数调用举例的汇编代码就剩去了
实际上主要是把jump
指令换成了函数调用的call
指令
*在汇编代码中,add
函数编译之后,代码先执行了一条push
指令和mov
指令;在结束时有执行了pop
和一条ret
指令。
这就是==压栈(Push)和出栈(Pop)操作
由于寄存器存不下多层函数调用堆叠的指令地址,引入了栈(stack)这个后进先出(LIFO, Last In First Out)的数据结构。
每次程序调用函数前,都把调用返回后的地址压入这个从内存中开辟的空间,叫做压栈,函数执行完毕后,再取出,叫做出栈。
实际上压栈的不只有函数调用完成后的返回地址,一些参数数据在寄存器不够用时也会压入栈中。整个函数所占用的所有内存空间叫做函数的栈帧(Stack Frame)。
rbp
和rsp
是两个维护函数调用的栈帧。
自己的思考
call
的同时进行了一次push rbp
把PC寄存器里关于完成函数调用后的返回地址以及参数数据压栈了;
ret
(return)的时候pop
把这部分数据出栈写回到PC寄存器;
在压栈和出栈之间,系统进行了函数的顺序执行,后出栈转到返回地址继续执行
没有调用其他函数只会被调用的函数成为叶子函数
"C语言代码-汇编语言-机器语言"实际上分为两部分:
编译(Compile)、汇编(Assemble)和链接(Link) gcc -o
可以生成对应的可执行文件,把多个目标文件以及调用的各种函数库链接起来。
通过装载器(Loader)把可执行文件装载(Load)到内存中,CPU从内存中读取指令和数据
可执行代码objdump
出的汇编代码不仅存放了汇编指令还有其他的数据,可执行文件和目标文件使用的叫ELF(Execuatable and Linkable File Format)的文件格式,也叫可执行与可链接文件格式。
在ELF中,函数名称likemain``add
…以及变量名和它们对应的地址都存储在符号表(Symbols Table)里。
ELF文件格式把各种信息分成Section保存起来。
链接器会扫描输入的目标文件,生成一个全局符号表。根据重定向表把所有不确定要跳转地址的代码根据符号表里存储的地址进行修正,最后将所有目标文件进行合并。装载器只需解析ELF文件加载到内存中供CPU执行。
Linux & Windows同一个程序不可以在两个系统下执行的原因
Windows使用的是PE(Protable Executable Format)的文件格式;
Linux使用的是ELF(Executable and Linkable File Format)的文件格式 不兼容。
程序装载所需的内存空间
装载器需要满足的两个要求:
可执行程序加载后占用的内存空间应该是连续的;
同时加载很多个程序,不能让程序自己规定在内存中加载的位置。
指令里用到的内存地址叫做虚拟内存地址(Virtual Memory Address),实际在内存硬件里面的空间地址叫做物理内存地址(Physical Memory Address)。维护一个虚拟内存到物理内存的映射表,这样实际程序指令执行的时候,会通过虚拟内存地址找到物理内存地址,然后执行。
找出一段连续的物理内存和虚拟内存进行映射的方法,叫做分段(Segmentation)。
可以用内存交换(Memory Swapping)解决,通过写到硬盘再读取,但会使整个机器变的卡顿(原因:硬盘的访问速度比内存慢很多)。
分段是分配一整段连续的空间给到程序
分页则是把整个物理内存空间切成一段段固定尺寸的大小,虚拟内存空间做同样的处理。这样一个连续并且尺寸固定的内存空间,叫做页(Page)。
这样在加载程序时不需一次性把程序加载到物理内存中。当CPU到读取一个特定的页,却发现数据没有加载到物理内存的时候,会触发缺页错误(Page Fault)。操作系统会捕捉到错误♂️从虚拟内存里读取出加载到内存里。
相同代码可以通过链接Linkable达到复用,相同的功能代码同样可以通过
动态链接
达到复用,减少内存空间的占用,这一讲就来解决这个问题
合并代码段 -> 静态链接(Static Link)
同样功能的代码在不同程序中不需要各占一份内存空间 -> 动态链接(Dynamic Link)
动态链接的是共享库(Shared Libraries)
Windows下,为.dll
文件,即Dynamic-Link Library(DLL, 动态链接库)
Linux下,为.so
文件,即Shared Object
这些共享库文件的指令代码必须是地址无关码(Position-Independent Code),即无论加载在哪个内存地址,都能正常执行。
地址无关码(大部分函数库)
向量加法
打印函数printf
…
地址相关码
绝对地址代码(Absolute Code)
利用重定位表的代码
在程序连接的时候,我们就把函数调用后要跳转访问的地址确定下来了
如果这个函数加载到一个不同的内存地址,跳转就会失败
…
这里引入了相对地址(Relatice Address)的概念来获得地址无关码
各种指令中使用到的内存地址,给出的不是一个绝对的地址空间,而是一个相对于当前指令偏移量的内存地址。
使用$ gcc lib.c -fPIC -shared -o lib.so
将lib.c编译成了一个动态链接库,也就是.so
文件,-fPIC
参数就是Position Independent Code。
call 400550
这里的@plt
就是程序链接表(Procedure Link Table)。
而跳转到的400550这个地址又指定了GLOBAL_OFFSET_TABLE+0x18
这个地址,这里的GLOBAL_OFFSET_TABLE
叫全局偏移表(GOT, Global Offset Table)。
不同的进程,调用同样的lib.so,各自GOT里面指向最终加载的动态链接库里面的虚拟内存是独立不同的。
完整汇编代码(转载作者:罗阿红 出处:http://www.cnblogs.com/luoahong/)
十进制转二进制
例如13->1101
原码表示法
最左侧一位做符号表示,0为正数,1为负数。
例:0011表示3;1011表示-3
补码表示法
在计算整个二进制值时,在左侧最高位前加一个负号。
例:二进制补码1011转换成十进制:-1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 1 * 2^0 = -5
补码表示法的二进制相加不用做任何处理,按照“满二进一,溢出丢弃”的方法
ASCII码是8位二进制数(8Bits=1Byte)也就是为什么一个字符(char)是一个字节了,而字符串(str)是两个字节则是因为\0
终止符。
最大的32位整数2147483647,用整数表示法,只需要32位,而字符串表示一共10个字符,需要十个8位二进制,也就是80位。这也是为什么采用二进制序列化而不是文本格式存储进行序列化。
Unicode就是一个字符集,包含了150种语言的14万个不同的字符。
UTF-8就是一种编码规则类似的还有UTF-16、UTF-32等。
出现乱码的原因
同样的文本,采用不同的编码存储下来,采用一种不同的编码方式解码和展示,就会出现乱码。
电报机
点信号(dot信号)
划信号(dash信号)
继电器(电驿) 用来解决导线过长引起的电阻对电信号的削弱。
“与”&
“或”|
“非” !
–数字电路中也被称作反向器(Inverter)
单比特逻辑运算
输入是两个单独的bit,输出是一个单独的bit。
插曲:“C语言”中
unsigned int
无符号整数,实际上表示无论高位是0还是1都是一个正数,不使用补码进行表示
个位数加法组合:0+0,0+1,1+0,1+1;
00和11的输出都是0(溢出丢弃),10和01的输出都是1,这正是异或门(XOR),实际上异或门就是最简单的整数加法。
1+1=10
仅有组合11才有进位是1,符合与门(AND)。
这就是***半加器(Half Adder)***
多考虑了前一位的进位,有三个bit的输入
加数和被加数仍是1bit的二进制数,但可以表示二位、四位、八位…的bit的相加(这里的二位、四位…类似于十进制的十位、百位…)
在计算机中,加法器是通过二进制数的每一位(个位、二位、四位…)单个位的数相加,得到该位的值以及进位信号,值保留在结果的该位置上,进位信号则提交给高一位的加法器继续运算。
全加器中等待上一位的进位信号,这个等待的时间叫做门延迟(Gate Delay)
先给一个示例13 * 9
实际上,二进制的乘法就是位移和加法。
在计算机中使用的方式是这样的:
先拿乘数最右侧的个位乘以被乘数,结果写入用来存放计算结果的开关里;
然后把被乘数左移一位(实际上是在数量级上乘以2^1 ,右侧空位用0补全),乘数右移一位(实际上是把本该和高一位相乘的数移到了最右侧的位置),仍然用乘数最右侧的位数乘以被乘数;
反复执行直到不能再移动。
同样是13 * 9的示例
在算法和数据结构中的术语:这样一个顺序乘法器硬件进行计算的时间复杂度是O(N),N是位数
降低乘法运算复杂度
并行加速⏩
电路并行(借助电路的天然的并行性,加大电路的复杂程度,同时获得高-低位的进位信号,以缩短门延迟)
定点数
浮点数
用科学计数法的方式,将指数位和系数位分开存储
float32
单精度浮点数
float64
双精度浮点数
符号位s
,所有的浮点数都是有符号的,0正1负,补码表示法
指数位e
,8Bit表示的整数空间是0~255,映射到有符号的就是-126~127
有效数位f
,双精度浮点数的有效数位多于单精度浮点数
任何浮点数都可以表示为 ( − 1 ) s ∗ 1. f ∗ 2 e (-1)^s*1.f*2^e (−1)s∗1.f∗2e
小数转换成二进制 举例9.1
法则:"乘以2,是否超过1,如果超出就记下1,循环操作♻️"
9 -> 1001
0.1的二进制表示:
也就是 0.000110011...
9.1 -> 1001.000110011… -> 1.001000110011… ✖️10^3
符号位s = 0
有效位f = 23
截取前23位
指数位e = 130
0~255中前127位映射到符号表示上为负数,那么10^3 为130
后面的章节与日后工作关系较小,就不做笔记了,仅做了解。谢谢!