深入理解计算机系统(Computer Systems: A Programmer's Perspective)

学习资料:

https://www.bilibili.com/video/BV1iW411d7hd  (字幕)

https://www.bilibili.com/video/BV1a54y1k7YE   (无字幕)

深入理解计算机系统书籍pdf : 链接: https://pan.baidu.com/s/1zWGfoxn0os0s9B3A2joM0Q 提取码: t4uc 

豆瓣9.5分的书籍,这是一本"入门"级别的书,覆盖面很广,从计算机的基本组成,二进制数据表示方式,到机器级别的指令,CPU工作方式,存储结构和优化,操作系统的虚拟内存管理,程序运行方式,I/O,网络、到程序性能优化和并行程序开发等,实验部分设计的也很精彩。 本视频对应于该书第3版,是原作者Randal E.Bryant的授课内容。 课件下载: https://pan.baidu.com/s/1ZKz5kYs4c1aWdmosEOx0bw 提取码:ru0t

Lecture01  Bits,Bytes,Integers:

这个课程涉及的都是x86-64位机器

位运算:

一个应用(表示集合):

位移操作:

左移的时候,只是用0填充,

右移分为 逻辑右移算术右移,其中的算术右移填充是用最高位进行填充的!

有符号整数:

例:

这里假定w=4(4位),

机器中能表示的所有情况:

0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111  总共有16中表示方法,

第一种方法(补码):B2T(现代计算机都是使用这种编码方式表示有符号整数)

0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 

   0        1     2       3        4     5        6      7        -8      -7      -6     -5       -4    -3       -2     -1  

第二种方法(反码):B2O

0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111  

  +0        1     2       3        4     5        6      7       -7     -6     -5       -4    -3       -2     -1     -0  

第三种方法(原码):B2S

0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111  

  +0      1     2       3        4        5      6        7      -0      -1     -2     -3       -4    -5       -6     -7 

参考: https://www.cnblogs.com/zach0812/p/11422697.html

有符号数的扩展,在不改变值的情况下扩展比特:

基本的规则是:将符号位复制到左侧来完成,

例如:

0110 -> 0 0110 ,它都是代表6 

1110 -> 1 1110 , 它都是代表-2 

截断

加法:

例如: w=4 ,

1101 + 0101  = 1 0010 

13           5             18%16 = 2 

乘法:

除法的右移

求相反数

将所有的比特反转 然后再加1  

整数运算总结:

为什么仍要使用无符号整数

无符号整数容易产生的问题:

既然无符号整数和有符号整数这么复杂,那么我们为什么还使用无符号整数,不应该去掉无符号整数吗(Python,Java就只有有符号整数)?

计算机的字长:

Byte Ordering :

Lecture02  Floating Point:

w = 1 -> 8 所能表示的小数:

import matplotlib.pyplot as plt
import copy


def PowerSetsRecursive(items):
    # the power set of the empty set has one element, the empty set
    result = [[]]
    for x in items:
        result.extend([subset + [x] for subset in result])
    return result


input_arrs = []
temp_arr = []
for i in range(1, 9):
    temp_arr.append(i)
    input_arrs.append(copy.deepcopy(temp_arr))

w = 1
for input_arr in input_arrs:
    subsets = PowerSetsRecursive(input_arr)
    datas = [0, ]  # 要绘制的数据
    for arr in subsets:
        if not arr:
            continue
        s = 0
        for i in arr:
            s += (1 / 2) ** i
        datas.append(s)
    # 对 datas 排序
    datas = sorted(datas)

    # 绘制datas
    ids = []  # 横坐标
    for i in range(len(datas)):
        ids.append(i)

    #print(ids)
    #print(datas)
    plt.scatter(ids,datas)
    plt.title(f"w={w}")
    plt.savefig(f"w={w}")
    #plt.show()
    w += 1
View Code

上述表示方法的局限:

它只能表示   形如 :   这样的小数,

其他的小数只能近似的表示,

例如,1/5 的表示,

用二进制表示,也有一些是循环的,就像10进制中的1/3 表示为0.3333333 [3]  

具体计算过程如下:

IEEE  浮点表示:

IEEE 浮点标准使用如下形式来表示一个数:

Note: Most Significant Bit,在二进制数中属于最高有效位,MSB是最高加权位,与十进制数字中最左边的一位类似。

三种浮点数的格式:(前两种,C中的float 和 C中的double )

01 Normalized 值:

 

关于范围:

容易知Exp 域 的范围是  [0,255]  0000 0000 -> 1111 1111 

又知,Bias = 127,即  0111 1111 

所以,实际的E 的范围是 [-127,128]     

 

 

 

总结:

frac域 就是 [1,2) 的小数,具体是 1.xxx...x (2进制),注:1.xxx...x的1 是隐含编码(默认就有了1,就不用再占用一个位置了)!  

exp域  是unsigned val ,需要E 额外 加上Bias(偏移),这里可以思考个问题,为什么不用补码来表示exp域?,为了更好的做比较

s 域 是0 或 1 

※关于为什么不用补码来表示exp域

It's just another way of doing negative numbers, instead of two's complement. There's no reason for 0 to actually be represented by zeroes.

There are 8 bits of exponent so 127 gives roughly the same positive and negative range.

 

 

02 Denormalized 值:

因为使用第一种normalized  values 要求M [1,2) ,所以就无法表示0。所以就有了第二种 Denormalized Values。

它的特征是 exp域全为0 ,它的frac 中没有隐含1, 所以frac 的编码和M完全等价, 

为什么要用 Denormalized 来表示接近0的浮点数,用范围 换精度  !

Special 值:

总结:

例子:

假设一个浮点数exp 是4个bits,frac 是3个bits 

画图来表示:

假设exp 3bit ,frac 2 bit 

完整范围:

-1 ~ 1 附近的视图放大后如下:

IEEE Encoding 的特殊性质:

  1. 浮点0 被编码为了整数0 
    1. All bits = 0 
  2.  除了NaN之外,可以比较所有的浮点数,具体方法是通过将其视为无符号的整数
    1. Must  first compare sign bits (必须首先比较最高位)
    2. Must consider -0 = 0 (必须考虑 -0 = 0 )  

 

舍入:

IEEE 有四种舍入模式:

最后一种模式是:4舍6入,5取偶   

二进制数字的舍入:

浮点数乘法:

两个符号位的操作就是异或的操作,可见只有相同的时候,异或才是1 ,不一样就是0 。

两个浮点数相加:

浮点数乘法可以交换,但不具有结合性,

浮点数加法可以交换,但不具有结合性,经常发生在一个很大的数字和一个很小数字的操作上,例如3.14 和 2^10 的操作,

总结: 浮点数是不满足结合律的, 而且一般是发生在两个相差很大的数字之间的情况,所以作为程序员一定要明白数字的变化范围,重新结合或改变运算的顺序结果可能是不相同的

现在有两种浮点数,有IEEE单精度和IEEE双精度浮点数。

Lecture03  Machine-Level Programming 01:Basics

History of Intel processors and architectures

Intel 系列的处理器俗称  x86 。  这是因为它的第一个芯片被称为8086。

然后跳过了8186,随后推出了8286,8386。

x86有时候又被称作CISC (Complex instruction set computer)。【在80年代早期有件大事,80年代又被称作RISC 和 CISC大战的年代,RISC是精简指令集计算机】

4核心芯片的构成:

8核处理器

顺便提一句的是除了  x86结构,还有ARM  ,它们是两个主要的两种设计。

总结:

Intel公司,AMD公司  都是使用x86结构,

ARM 实际是 Acorn RISC Machine ,Acorn 早期是一家公司,它们在早期决定制造自己的个人电脑,它们不打算从Intel购买芯片,最后它破产了,但是它们提出了一个相当不错的指令集(Advanced RISC),非常高效简单。现在ARM是一家总部位于剑桥的公司,它出售设计而不是芯片。

C,assembly,machine code

Assembly Basic:Registers,operands,move

16个寄存器,可以用来保存整数和指针。%r 代表的是64位, %e代表的是32位。 

movq 指令

q of movq is quad ,意思是4个字节,64位。

注: 出于 对硬件设计者的方便,不允许从Mem 复制到 Mem,   而且Imm (立即数)也不能作为Dest。  

这个例子展示了非常简单的内存引用,  

关于why S is 1,2,4,8, 因为如果这是一个数组索引,必须通过数据类型的字节数来缩放索引值,如果它是一个int,就必须缩放4倍,如果是long,就缩放8倍,这也是它们存在的原因。 

上面是  汇编代码中内存引用的基本格式,如果不使用某些字段,可以删除其中的一些字段,如下:

Arithmetic & logical operations 

lea 指令:

这里是将 3*%rdi 存储到  %rax 中,然后再讲%rax 左移2位,这也是lea 和 mov 的区别,lea 是不使用内存引用!

总之一句话,lea without memory reference, lea 常用于 整数和地址的运算~    

这种做法比直接乘12 会好,因为乘法运算会很慢, 

注:这里Src在前,Dest在后,

  

一个例子:

lea 的两种用法(其实是一种):

01 Computing arithmetic expressions of the form x + k*y

#include <stdio.h>

long m12(long x){
    return x*12;
}

int main(){

    return 0;
}
01_arthimetic.c

02 Computing addresses 

#include <stdio.h>

long* compute_address01(long* arr){
    long *p = &arr[0];
    return p;
}

long* compute_address02(long* arr){
    long *p = &arr[1];
    return p;
}

long* compute_address03(long* arr){
    long *p = &arr[2];
    return p;
}

int main()
{
    long arr[] = {0,0,0,0,0};
    compute_address01(arr);
    compute_address02(arr);
    compute_address03(arr);

    return 0;
}
02_compute_address.c

使用 gcc xxx.c -Og -S 就可以产生对应的 xxx.s 的汇编代码了。 

Lecture04  Machine-Level Programming 02:Control

现在有几个问题,

寄存器(Reg) 是 内存(Mem) 的一部分? No,

寄存器(Reg) 是缓存(Cache) 的一部分? No,

寄存器是CPU内部用来存放数据的一些小型存储区域 。  

本节主要是表述 如何控制机器级别指令的执行顺序,以及如何使用这些技术来实现基本的条件语句,循环和switch 语句 等。 

Control:  Condition codes 

有一些指令,它们唯一的作用就是设置条件码,

这里需要提醒的是,在机器级编码中的参数顺序通常与高级语言中的顺序相反,例如:cmpq Src2,Src1, 我们在高级语言中一般是 cmp Src1,Src2。

test 指令的唯一目的也是设置条件标志,

movzbl 指令,是从%al 扩展到 %eax ,eax 虽然是只有32位,但是在x86的世界里,如果对低32位做了操作,那么也相当于对高32位做了操作,

Conditional branches 

由于条件分支的汇编代码   和   C的goto 语句  很类似,由因为汇编代码可读性较差,所以下面都是用goto 形式来表述 条件分支的汇编代码。 注:(C中不建议使用goto语句)

为什么不能一直使用Conditional Move?

1,进行两种情况的计算可能是一个非常非常糟糕的主意,

2,有时候确实不能这样做,例如要保证p非空指针,

3, 执行程序会影响其他分支的结果 ,例如第三种情况

Loops  

两种方式,生成While 循环的汇编代码:

 

第三种循环时for 循环:

Switch Statements   

switch 语句的汇编形式,

switch 的简单用法:

我们知道可以用if-else 代替 switch,所以,你可能觉得switch 的汇编代码也是 if-else 类似的汇编代码,实际上不是的!

注: ja : Jump above ,这里使用ja 而不是使用jg(jump greater),是因为如果x是负数,进行无符号的比较也会变为很大的正数。 总之,如果x 大于6,或者小于0都会直接跳到 default!   

 

问题:

如果x是负数,通常会加上一个偏置Bias 变为正数

如果只有两个case ,case 0 和 case 10000 ,难道要建立 10000个项的表吗? 的确是的,所以编译器会优化成为if-else 的汇编代码形式,   

虽然有时switch用到不多,但是有时候也是可以带来性能的提升,因为switch表是O(1) 的时间复杂度,if-else 是线性的复杂度O(n)  ,

而且,如果case是稀疏的话,它会设置一个条件树,进行二分搜索,可以达到时间复杂度为: O(logn)的级别。 

不管哪种都是优于if -else 的,   

总结:

基本上有三个条件控制,

1,使用条件跳转到代码的不同位置

2,是使用Condition moves (先把所有情况都计算出来,再决定跳到哪个)

3,  是使用Jump table   

Lecture05  Machine-Level Programming 02:Procedures 

原文地址:https://www.cnblogs.com/zach0812/p/14146495.html