计算机组成 -- 二进制编码
补码表示法
原码表示法
0011为3,1011为-3
缺点:0000和1000都表示为0
浪费 + 模凌两可
由此诞生了补码表示法,其实就是一个简单的翻转而已
用补码表示负数,使得整数的相加变得容易,不需要做任何特殊处理,当成普通的二进制相加即可
字符串
ASCII码类似一个字典,用8位二进制中的128个不同的数字,映射到128个不同的字符里
a在ASCII里面是第97个,二进制为0110 0001,对应的十六进制为0x61
字符串9用0011 1001来表示,字符串15用0011 0001和0011 0101来表示,占用更多的空间
因此存储数据的时候,要采用二进制序列化的形式,
而不是简单地把数据通过CSV或者JSON这样的文本格式存储来进行序列化
不管是整点数,还是浮点数,采用二进制序列化比存储文本能节省不少空间
字符集(Charset)和字符编码(Character Encoding)
字符集:字符的集合
Unicode是字符集,包含150种语言的14万个字符
字符编码:对于字符集里的这些字符,怎么用二进制表示出来的一个字典
Unicode可以用UTF-8、UTF-16、U ...
计算机组成 -- 动态链接
静态链接
把对应的不同文件内的代码段合并在一起,成为最后的可执行文件,可以做到代码在开发阶段的复用
很多程序都需要通过装载器装载到内存里面,里面链接好的同样的功能代码,也都需要再装载一遍,再占一遍内存空间
动态链接
动态链接过程中,需要链接的不是存储在硬盘上的目标文件代码,而是加载到内存中的共享库(Shared Libraries)
加载到内存中的共享库会被很多程序的指令调用
Windows的共享库文件是.dll(Dynamic-Link Libary)文件;Linux的共享库文件是.so(Shared Object)文件
地址无关 + 相对地址
要在程序运行时共享代码,这些机器码必须是地址无关的,即编译出来的共享库文件的指令代码,是地址无关码
地址无关码(PIC):Position-Independent Code,无论加载到哪个物理内存地址,都能够正常执行
大部分函数库都能做到地址无关,因为都是接受特定的输入,进行确定的操作,然后给出返回结果
对于所有动态链接共享库的程序来讲
虽然共享库用的都是同一段物理内存地址
但在不同的应用程序里,共享库所在的虚拟内存地址是不同的
不应该要求动 ...
计算机组成 -- 段 + 页
程序装载
背景
通过链接器,把多个目标文件合并成一个最终可执行文件
运行可执行文件时,其实是通过一个装载器,解析ELF或者PE格式的可执行文件
装载器会把对应的指令和数据加载到内存里面,让CPU去执行
装载器需要满足两个条件
可执行程序加载后占用的内存空间应该是连续的
执行程序时,程序计数器是顺序地一条一条指令执行下去
需要同时加载很多个程序,并且不能让程序自己规定在内存中加载的位置
内存地址
虚拟内存地址:指令里用到的内存地址
物理内存地址:内存硬件里的空间地址
一个思路
在物理内存里面找一段连续的内存空间,分配给装载的程序
然后把这段连续的内存空间地址和整个程序指令里指定的内存地址做一个映射
程序里有指令和各种内存地址,而我们只需要关心虚拟内存地址即可
对任何一个程序来说,它所看到的都是同样的内存地址
维护一个虚拟内存到物理内存的映射表
实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行
因为是连续的内存地址空间,只需要维护映射关系的起始地址和对应的空间大小即可
内存分段
分段(Segmentation):找出一段连续的物理内存和虚拟内存地址 ...
计算机组成 -- ELF + 静态链接
代码拆分源代码add_lib.c12345// add_lib.cint add(int a, int b){ return a+b;}
link_example.c1234567891011// link_example.c#include <stdio.h>int main(){ int a = 10; int b = 5; int c = add(a, b); printf("c=%d\n", c);}
gcc + objdump123$ gcc -g -c add_lib.c link_example.c$ objdump -d -M intel -S add_lib.o$ objdump -d -M intel -S link_example.o
add_lib.o1234567891011121314151617add_lib.o: 文件格式 elf64-x86-64Disassembly of section .text:0000000000000000 <add>:/ ...
计算机组成 -- 函数调用
程序栈123456789101112131415// function_example.c#include <stdio.h>int static add(int a, int b){ return a+b;}int main(){ int x = 5; int y = 10; int u = add(x, y);}
12$ gcc -g -c function_example.c$ objdump -d -M intel -S function_example.o
1234567891011121314151617181920212223242526272829303132333435363738......0000000000000000 <add>:......int static add(int a, int b){ 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: ...
计算机组成 -- goto
CPU执行指令
CPU是由一堆寄存器组成的,而寄存器是由多个触发器(Flip-Flop)或者锁存器(Latches)组成的简单电路
触发器和锁存器是两种不同原理的数字电路组成的逻辑门
N个触发器或者锁存器,就可以组成一个N位的寄存器,能保存N位的数据,64位的Intel服务器,寄存器就是64位的
寄存器分类
PC寄存器(Program Counter Register),也称为指令地址寄存器(Instruction Address Register)
用来存放下一条需要执行的计算机指令的内存地址
指令寄存器(Instruction Register)
用来存放当前正在执行的指令
条件码寄存器(Status Register)
用里面的一个个标志位(Flag),存放CPU进行算术或者逻辑计算的结果
其它
整数寄存器、浮点数寄存器、向量寄存器、地址寄存器、通用寄存器
程序执行
CPU会根据PC寄存器里面的地址,从内存里把需要执行的指令读取到指令寄存器里面执行
然后根据指令长度自增,开始顺序读取下一条指令,一个程序的指令,在内存里面是连续保存的,也会一条条顺序加载
特殊指令,如J类指令(跳转 ...
项目立项 -- 保证不亏钱
问题抽象一下:假设有n个坑,第$i$个坑投注了$X_i$,倍率为$Y_i$,怎样设置倍率才能保证不亏钱?
推导过程如下:
假设某场第$k$个坑赔最少,最少赔付记为$min = (X_k \times Y_k)$
则$(X_k \times Y_k) \leq (X_l \times Y_l) \quad l \in [1,n]$
易得$\frac{X_k \times Y_k}{Y_l} \leq X_l \quad l \in [1,n]$
而收入为$(\sum_{i=1}^n{X_i}) \geq (\sum_{i=1}^n{\frac{X_k \times Y_k}{Y_i}}) = (\sum_{i=1}^n{\frac{1}{Y_i}}) \times (X_k \times Y_k) = (\sum_{i=1}^n{\frac{1}{Y_i}}) \times min$
而不亏钱,只要保证$(\sum_{i=1}^n{X_i}) \geq min$即可,而由上易知,$(\sum_{i=1}^n{\frac{1 ...
计算机组成 -- 指令
CPU + 计算机指令
硬件的角度
CPU是一个超大规模集成电路,通过电路实现了加法、乘法乃至各种各样的处理逻辑
软件工程师的角度
CPU就是一个执行各种计算机指令的逻辑机器
计算机指令是一门CPU能听懂的语言,也称为机器语言
不同的CPU能够听懂的语言不太一样,两种CPU各自支持的语言,就是两组不同的计算机指令集
计算机程序平时是存储在存储器中,这种程序指令存储在存储器里面的计算机,叫作存储程序型计算机
代码 -> 机器码(编译 -> 汇编)1234567// test.cint main(){ int a = 1; int b = 2; a = a + b;}
编译(Compile)成汇编代码:把整个程序翻译成一个汇编语言(ASM,Assembly Language)的程序
汇编:针对汇编代码,用汇编器(Assembler)翻译成机器码(Machine Code)
机器码由0和1组成的机器语言表示,一串串的16进制数字,就是CPU能够真正认识的计算机指令
汇编代码 + 机器码12345678910$ gcc --help-c ...
计算机组成 -- 提升性能
CPU的功耗12CPU time = 时钟周期时间(Clock Cycle Time) × CPU时钟周期数(CPU Cycles) = 时钟周期时间(Clock Cycle Time) × 指令数 × 每条指令的平均周期数(Cycles Per Instruction,CPI)
80年代开始,CPU硬件工程师主要着力提升CPU主频,到功耗是CPU的人体极限
CPU,一般被叫做超大规模集成电路,这些电路,实际上都是一个个晶体管组合而成的
CPU计算,实际上是让晶体管里面的『开关』不断地去打开或关闭,来组合完成各种运算和功能
如果要计算得快,有两个方向:增加密度(7nm制程)、提升主频,但这两者都会增加功耗,带来耗电和散热的问题
密度 -> 晶体管数量
主频 -> 开关频率
如果功耗增加太多,会导致CPU散热跟不上,此时就需要降低电压(低压版CPU)
1功耗 ≈ 1/2 × 负载电容 × 电压的平方 × 开关频率 × 晶体管数量
并行优化 – 阿姆达尔定律1优化后的执行时间 = 受优化影响的执行时间 / 加速倍数 + 不受影响的执行时间
其它
加速大概率事件(C ...
计算机组成 -- 性能
性能指标
响应时间(Response time)、执行时间(Execution time)
执行一个程序,需要花多少时间
吞吐率(Throughput)、带宽(Bandwidth)
单位时间范围内,能处理多少数据或执行多少指令,可以通过多核、集群等方式来提升吞吐率
性能 = 1/响应时间
CPU时钟
time命令
real time
Wall Clock Time/Elapsed Time,运行程序整个过程中流逝掉的时间
user time
在用户态运行指令的时间
sys time
在操作系统内核里运行指令的时间
程序实际花费的CPU执行时间:CPU time = user time + sys time
程序实际占用的CPU time一般比Elapsed Time少(单核情况下)
123456$ time seq 1000000 | wc -l1000000real 0m0.024suser 0m0.018ssys 0m0.005s
程序实际花了0.024s,CPU time只有0.018s+0.005s=0.02 ...