[深入理解计算机系统] 02 信息的表示和处理 笔记

[深入理解计算机系统] 02 信息的表示和处理 笔记

计算机 ComputerScience|计算机原理 ComputerSystem


@ZYX 写于2020年07月12日

第二章 信息的表示和处理

前言

  • 我们研究三种最重要的数字表示
    1. 无符号(unsigned)编码基于传统的二进制表示法,表示大于或者等于零的数字
    2. 补码(two’s-complement)编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字
    3. 浮点数(floating-point)编码是表示实数的科学记数法的以二为基数的版本
  • 计算机的表示法是用有限数量的位来对一个数字编码,因此,当结果太大以至不能表示时, 某些运算就会溢出(overflow)
  • 浮点运算有完全不同的数学属性。
    • 虽然溢出会产生特殊的值但是一组正数的乘积总是正的
    • 由于表示的精度有限,浮点运算是不可结合的
      (3.14+1e20)-1e20==0.0
      3.14+(1e20-1e20)==3.14
  • 整数运算浮点数运算会有不同的数学属性是因为它们处理数字表示有限性的方式不同:
    • 整数的表示只能编码一个较小的数值范围,但是这种表示是精确的
    • 浮点数可以编码一个较大的数值范围,但是这种表示只是近似的
  • Java语言创造了一套新的数字表示和运算标准。C标准的设计允许多种实现方式,而Java标准在数据的格式和编码上是非常精确具体的

2.1 信息存储

  • 大多数计算机使用8位的块,或者字节(byte) ,作为最小的可寻址的存储器单位 , 而不是在存储器中访问单独的
    • 虚拟存储器(virtual memory)
      机器级程序将存储器视为一个非常大的字节数组,称为虚拟存储器
    • 地址(address)
      存储器的每个字节都由一个唯一的数字来标识,称为它的地址
    • 虚拟地址空间(virtual address space)
      所有可能地址的集合称为虚拟地址空间

      • 这个虚拟地址空间是一个展现给机器级程序概念性映像
        • 实际实现是将随机访问存储器(RAM)磁盘存储器特殊硬件操作系统软件结合起来,为程序提供一个看上去统一的字节数组
      • 接下来的几章,我们将讲述编译器运行时系统是如何将存储器空间划分为更可管理的单元,以存放不同的程序对象(program object)
        • 程序对象:程序数据、指令和控制信息。
        • 这种管理完全是在虚拟地址空间里完成的

2.1.1 十六进制表示法

  • 在C语言中,以Ox或0X开头的数字常量被认为是十六进制的值。
    • 字符‘A’〜‘F’既可以是大写,也可以是小写,甚至是大小写混合

2.1.2 字与数据大小

  • 每台计算机都有一个字长(word size) ,指明指针数据标称大小(nominal size)
    • 对于一个字长为ω位的机器而言 :
      1. 虚拟地址的范围为0〜$2^{\omega}-1$
      2. 程序最多访问$2^\omega$个字节。
    • 32位和64位
      • 向后兼容:大多数64位机器都能兼容32位机器编译的程序
        基本C 数据类型的典型大小(以字节为单位)。 分配的字节数受程序是如何编译的影响而变化。 本图给出的是32 位和64 位程序的典型值
  • ISO C99引入了大小固定的数据类型:
    比如int32_t固定为4字节 in64_t固定为8字节

    • 数据大小固定,不随编译器和机器变化
  • 程序员应该力图使他们的程序在不同的机器和编译器上是可移植的
    • 可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感
    • C语言标准对不同数据类型的数字范围设置了下界,但是却没有上界

2.1.4 寻址和字节顺序

  • 对于跨越多字节的程序对象,我们必须建立两个规则:
    1. 这个对象的地址是什么,
    2. 存储器中如何排列这些字节
    • 在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址
  • 排列字节的规则:
    • 大端法(big endian)
      最高有效字节在最前面的方式,称为大端法(big endian)

      • 大多数IBM和Sun Microsystems的机器都采用这种规则
    • 小端法(little endian)
      最低有效字节在最前面的方式,称为小端法(little endian)

      • 大多数Intel兼容机都釆用这种规则
    • 双端法(bi-endian)
      许多比较新的微处理器使用双端法(bi-endian),也就是说可以把它们配置成作为大端或者小端的机器运行

      • 一旦选择了特定的操作系统, 字节顺序就固定了
        • 比如ARM架构处理器使用双端法,但是跑了安卓和iOS后就是小端法
    • 对于大多数应用程序员来说,他们机器所使用的字节顺序是完全不可见的,无论为哪种类型的机器编译的程序都会得到同样的结果
      • 不过有时候,字节顺序会成为问题:
        1. 网络传输
          此时应用程序的代码编写必须遵守已建立的关于字节顺序的规则,以确保:

          1. 发送方机器将它的内部表示转换成网络标准,
          2. 接收方机器则将网络标准转换为它的内部表示
        2. 当阅读表示整数数据的字节序列时
          • 通常在检查机器级程序时会出现这种情况
            8G483bd:      01      05      64      94      04      08      add      %eax,      0x8049464
          • 这一行是由反汇编器(disassembler)生成的,
            • 反汇编器是一种确定可执行程序文件所表示的指令序列的工具
        3. 当编写规避正常的类型系统的程序时
          • 强制类型转换(cast)与union
  • show_bytes函数:
typedef unsigned char *byte_pointer;
void show_bytes(byte_pointer start, int len) {
    int i;
    for(i = 0; i < len; i++)
        printf("%.2x", start[i]);
    printf("\n");
}

2.1.4 表示字符串

  • C语言中字符串被编码为一个以null (其值为0)字符结尾的字符数组
    • 字符都由某个标准编码来表示,最常见的是ASCII字符码
  • 文本数据比二进制数据具有更强的平台独立性
    • 在使用ASCII码作为字符码的任何系统上都将得到相同的结果与字节顺序和字大小规则无关

2.1.5 表示代码

  • 不同的机器类型使用不同的且不兼容的指令和编码方式
  • 二进制代码是不兼容的
  • 从机器的角度来看,程序仅仅只是字节序列

2.1.6 布尔代数简介

2.1.7 C语言中的位级运算

|就是OR (或),&就是AND (与),〜就是NOT (取反),而^就是EXCLUSIVE-OR (异或)

2.1.8 C语言中的逻辑运算

  • C语言还提供了一组逻辑运算符||、&&和!,分别对应于命题逻辑中的OR、AND和NOT 运算。
  • 逻辑运算很容易和位级运算相混淆,但是它们的功能是完全不同
    真值 返回值
    所有非零的参数 TRUE 1
    参数0 FALSE 0

2.1.9 C语言中的移位运算

  • C语言还提供了一组移位运算,以便向左或者向右移动位模式
  • 左移:
    • 对于一个位表示为$[x_{\omega-1}, x_{\omega-2}, …, x_0]$的操作数x,C表达式x«k会生成一个值,其位表示为$[x_{\omega-k-1}, x_{\omega-k-2}, …, x_0, 0, …, 0]$
      1. x向左移动k位
      2. 丢弃最高的k位
      3. 并在右端补k个0
    • 移位量应该是一个0〜ω-1之间的值
    • 移位运算是从左至右可结合的
      x«j«k==(x«j)«k
  • 右移
    • 一般而言,机器支持两种形式的右移:逻辑右移算术右移
      • 逻辑右移在左端补k个0,得到的结果是$[0, …, 0, x_{\omega-1}, x_{\omega-2}, …, x_k]$
      • 算术右移是在左端补k个最髙有效位的值,得到的结果是$[x_{\omega-1}, …, x_{\omega-1}, x_{\omega-1}, x_{\omega-2}, …, x_k]$
        • 它对有符号整数运算非常有用。
    • C语言标准并没有明确定义应该使用哪种类型的右移
      • 对于unsigned数据,右移必须是逻辑的
      • 对于有符号数据,算术/逻辑右移都可以
      • 实际上,几乎所有的编译器/机器组合都对有符号数据使用算术右移,且许多程序员也都假设这样。
    • 另一方面,Java对于如何进行右移有明确的定义
      • x>>k会将x算术右移k个位置
      • x>>>k会对x做逻辑右移

2.2 整数表示

编码整数的两种不同方式:一种只能表示非负数,而另一种能够表示负数、零和正数
– 术语表
图2-8 整数的数据与算术操作术语。下标w表示数据表示中的位数

2.2.1 整型数据类型

图2-9 32 位程序上C 语言整型数据类型的典型取值范围
图2-10 64 位程序上C 语言整型数据类型的典型取值范围
– 一个值得注意的特点是取值运围不是对称的——负数的范围比整数的范围大1。
– C语言标准定义了每种数据类型必须能够表示的最小的取值范围
图2-11 C 语言的整型数据类型的保证的取值范围。C 语言标准要求
这些数据类型必须至少具有这样的取值范围

2.2.2 无符号数的编码

  • 假设一个整数数据类型有ω位。我们可以将位向量写成 $\vec \omega$ 表示整个向量,或者写成$[x_{\omega-1}, x_{\omega-2}, …, x_0]$来表示向量中的每一位
  • 原理: unsigned数编码的定义
    对向量$\vec x=[x_{\omega-1}, x_{\omega-2}, …, x_0]$:
    $B2U_\omega(\vec x)=\sum_{i=0}^{\omega-1}x_i·2^i$
  • 函数$B2U_\omega$能够被定义为一个映射 ${0,1}^{\omega}$→{${0, …, 2^\omega-1}$}
  • 原理: unsigned编码的唯一性
    • 函数$B2U_\omega$是双射(bijection)

2.2.3 补码编码

  • 最常见的有符号数的计算机表示方式就是补码(two’s-complement) 形式
    • 将字的最高有效位解释为负权(negative weight).
  • 原理:补码编码的定义
    对向量$\vec x=[x_{\omega-1}, x_{\omega-2}, …, x_0]$:
    $B2T_\omega(\vec x)=-x_{\omega -1}2^{\omega-1}+\sum_{i=0}^{\omega-2}x_{i}2^{i}$

    • 最高有效位$x_{\omega-1}$也称为符号位,它的“权重”为一2^{\omega-1}
      • 置1,表负,置0,非负。
  • 原理:补码编码的唯一性
    函数$B2T_{\omega}$是一个双射。

    • 对于每个数$x$
      • 满足$TMin_{\omega}\le x\le TMax_{\omega}$ 则$T2B_{\omega}(x)$是x的(唯一的)ω位模式
        图2-14 重要的数字。图中给出了数值和十六进制表示
  • C语言标准并没有要求要用补码形式来表示有符号整数,但是几乎所有的机器都是这么做的
    • C 库中的文件<limits.h>定义了一组常量,来限定编译器运行的这台机器的不同整型数据类型的取值范围。比如,它定义了常量INT_MAX, INT_MIN, 和UINT_MAX它们描述了有符号和无符号整数的范围
  • Java 标准是非常明确的。它要求采用补码表示。在Java 中,单字节数据类型称为byte而不是char

2.2.4 有符号数和无符号数之间的转换

  • C 语言允许在各种不同的数字数据类型之间做强制类型转换。
  • 对于大多数C 语言的实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式不变
  • 原理:补码转换为无符号数
    对满足$TMin_{\omega}\le x\le TMax_{\omega}$的$x$有:
    $T2U_{\omega}(x)= \begin{cases}a &\text{, }x<0\\ c &\text{, } x \ge 0 \end{cases}$
  • 推导:补码转换为无符号数
    $B2U_{\omega}(T2B_{\omega}(x))=T2U_{\omega}(x)=x+x_{\omega-1}2^{\omega}$
  • 原理:无符号数转换为补码
    对满足$0\le u \le UMax_{\omega}$的$u$有:
    $U2T_{\omega}(u)=\begin{cases} u &\text{, }u \le TMax_{\omega}\\u-2^{\omega} &\text{, } u>TMax_{\omega} \end{cases}$
  • 推导:无符号数转换为补码
    $U2T_{\omega}(u)=-u_{\omega -1}2^{\omega}+u$

2.2.5 C 语言中的有符号数与无符号数

  • 通常,大多数数字都默认为是有符号的
  • 当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。
    • 对于像<和>这样的关系运算符来说,它会导致非直观的结果
      图2-19 C 语言的升级规则的效果

2.2.6 扩展一个数字的位表示

  • 零扩展(zero extension): 将一个无符号数转换为一个更大的数据类型,我们只要简单地在表示的开头添加0:
    原理:无符号数的零扩展
    定义宽度为ω的位向量$\vec u = [u_{\omega-1}, u_{\omega}, …, u_{0}]$和宽度为 $\omega’$的位向量$\vec u’=[0, …, 0, u_{\omega-1}, u_{\omega}, …, u_{0}]$ 其中ω’>ω。则$B2U_{\omega}(\vec u)=B2U_{\omega ‘}(\vec u’)$
  • 符号扩展(sign extension) : 要将一个补码数字转换为一个更大的数据类型,可以在表示中添加最高有效位的值
    原理:补码数的符号扩展
    定义宽度为ω的位向量$\vec{x}=[x_{u-1}, x_{w-2}, \cdots, x_{0}]$和宽度为ω的位向量$\vec{x}^{\prime}=[x_{w-1}, \cdots, x_{w-1}, x_{w-1}, x_{w-2}, \cdots, x_{0}]$, 其中ω’>ω, 则$B 2 T_{w}(\bar{x})=B 2 T_{w^{\prime}}\left(x^{\prime}\right)$

2.2.7 截断数字

  • 原理:截断无符号数
      1. $\vec x=[x_{\omega-1}, x_{\omega-2}, …, x_0]$
      2. $\vec x’$是将其截断为k位的结果:$\vec x’=[x_{k-1}, x_{k-2}, …, x_{0}]$
      3. $x=B2U_{w}(\vec x)$
      4. $x’=B2U_{k}(\vec x’)$
    • 则 $x’=x \mod 2^{k}$
  • 原理:截断补码数值
      1. $\vec x=[x_{\omega-1}, x_{\omega-2}, …, x_0]$
      2. $\vec x’$是将其截断为k位的结果:$\vec x’=[x_{k-1}, x_{k-2}, …, x_{0}]$
      3. $x=B2U_{w}(\vec x)$
      4. $x’=B2T_{k}(\vec x’)$
    • 则 $x’=U2T_{k}(x \mod 2^{k})$
  • 推导:截断补码数值
    $B2U_{w}([x_{\omega-1}, x_{\omega-2}, …, x_0]) \mod 2^{k}=B2U_{k}[x_{k-1}, x_{k-2}, …, x_{0}]$

2.2.8 关于有符号数与无符号数的建议

unsigned对unsigned,不要混用有符号和无符号

2.3 整数运算

2.3.1 无符号加法

  • 原理:无符号数加法 $+_{w}^{u}$
    对$0 \le x, y<2^{w}$的x和y有:
    $x+_{w}^{u}=\begin{cases} x+y &\text{, } x+y<2^{w} \\x+y-2^{w} &\text{, }2^{w} \le x+y < 2^{w+1} \end{cases}$
  • 运算溢出,是指完整的整数结果不能放到数据类型的字长限制中去
  • 原理:检测无符号数加法中的溢出
    对:在范围$0 \le x,y \le UMax_{w}$中的x和y
    令:$s=x+^{u}_{w}y$
    则:对计算s,当且仅当s<x(或者等价地s<y)时,发生了溢出
  • 推导:检测无符号数加法中的溢出
    1. 如果s 没有溢出,我们能够肯定s≥x
    2. 如果s 确实溢出了,我们就有$s=x+y-2^{w}<x$
  • 原理:无符号数求反
    对:满足$0 \le x < 2^{w}$的任意x
    其w位的无符号逆元$-^{u}_{w}x=\begin{cases} x &\text{, } x=0 \\2^{w}-x &\text{, } x>0\end{cases}$

2.3.2 补码加法

  • 原理:补码加法
    对:满足$-2^{w-1}\le x,y \le 2^{w-1}-1$的整数x和y
    有$x+^{t}_{w}=\begin{cases} x+y-2^{w} &\text{, } 2^{w-1}\le x+y 正溢出 \\x+y &\text{, } -2^{w-1}\le x+y \le 2^{w-1} 正常 \\x+y+2^{w} &\text{, } x+y<-2^{w-1} 负溢出\end{cases}$
  • 推导:补码加法
    $x+^{t}_{w}y=U2T_{w}(T2U_{w}(x) +^{u}_{w} T2U_{w}(y))$
  • 原理:检测补码加法中的溢出
    对满足$TMin_{w} \leqslant x, \quad y \leqslant TMax_{w}$的x和y
    令$s=x+^{t}_{w}y$

    • 正溢出 当且仅当x>0, y>0(或$x+y<TMax_{w}$), s≤0
    • 负溢出 当且仅当x<0, y<0, s≥0

2.3.3 补码的非

  • 原理:补码的非
    对满足$TMin_{w} \le x \le TMax_{w}$的x
    其补码非$-^{t}_{w}x=\begin{cases} TMin_{w} &\text{, } x=TMin_{w} \\-x &\text{, } x>TMin_{w} \end{cases}$

2.3.4 无符号乘法

  • 原理:无符号数乘法
    对满足$0 \le x,y \le UMax_{w}$的x和y
    有$x*^{u}_{w}y=(x·y)\mod 2^{w}$

2.3.5 补码乘法

  • 原理:补码乘法
    对满足$TMin_{w} \le x,y \le TMax_{w}$的x和y
    有$x*^{t}_{w}y=U2T_{w}((x·y)\mod 2^{w})$
  • 原理:无符号和补码乘法的位级等价性
    给定长度为w的位向量$\vec x, \vec y$
    用补码形式的位向量表示来定义整数x和y: $x=B2T_{w}(\vec x), y=B2T_{w}(\vec y)$
    用无符号形式的位向量表示来定义非负整数x’和y’: $x’=B2U_{w}(\vec x), y’=B2U_{w}(\vec y)$
    则$T2B_{w}(x*^{t}_{w}y)=U2B_{w}(x’*^{u}_{w}y’)$

2.3.6 乘以常数

  • 以往,在大多数机器上,整数乘法指令相当慢,需要10个或者更多的时钟周期,然而其他整数运算(例如加法、减法、位级运算和移位)只需要1个时钟周期。因此,编译器使用了一项重要的优化,试着用左移和加法运算的组合来代替乘以常数因子的乘法。
  • 原理:与2的幂相乘的无符号乘法
    C 变量x和k有无符号数值x和k,
    且$0 \le k<w$
    则C 表达式x<<k产生数值:$x*^{u}_{w}2^{k}$

    • 固定大小的补码算术运算的位级操作与其无符号运算等价
      • 产生$x*^{t}_{w}2^{k}$
  • 这样就可以通过快速展开乘数,把乘法变成多次移位
    • x * 14=> (x<<3)+(x<<2)+(x<<1) 因为$14=2^{3}+2^{2}+2^{1}$
    • 甚至可以变成 (x<<4)-(x<<1) 因为$14=2^{4}-2^{1}$

2.3.7 除以2的幂

  • 在大多数机器上,整数除法要比整数乘法更慢——需要30个或者更多的时钟周期。
  • 除以2的幂也可以用右移运算来实现
    • 无符号用逻辑移位
    • 补码数用算术移位
  • 整数除法总是舍入到零
  • 原理:除以2 的幂的无符号除法
    C 变量x 和k 有无符号数值$x$和$k$
    且$0 \le k<w$
    x>>k产生数值$\lfloor x/2^{k} \rfloor$
  • 原理:除以2 的幂的补码除法,向下舍入
    C变量x 和k 分别有补码值x 和无符号数值k
    且$0 \le k<w$
    则当执行算术移位时,C表达式x>>k产生数值$\lfloor x/2^{k} \rfloor$

    • 对于x>0, 变量x 的最高有效位为0, 所以效果与逻辑右移是一样的。
      • 对于非负数来说,算术右移k位与除以$2^{k}$是一样的
  • 原理:除以2 的幂的补码除法,向上舍入
    C 变量x 和k 分别有补码值x和无符号数值k,
    且$0 \le k<w$
    则当执行算术移位时,C 表达式(x+(1<<k)-1)>>k=>$\lfloor x/2^{k} \rfloor$。

    • 也就是说,通过给x增加一个偏量y-1
      然后再将除法向下舍人
      当y整除x时,我们得到q,否则,就得到q+1
  • 算术右移的代码
    (x<0 ? x+(1<>k

2.3.8 关于整数运算的最后思考

  • 计算机执行的“整数”运算实际上是一种模运算形式

2.4 浮点数

  • 浮点表示对形如$V=x \times 2^{y}$的有理数进行编码
  • IEEE读作 eye-triple-ee

2.4.1 二进制小数

  • 形如$b_{m}b_{m-1}\dots b_{1}b_{0}.b_{-1}b{-2}\dots b_{-n+1}b_{-n}$
    $b=\sum^{m}_{i=-n}2^{i}\times b_{i}$

    • 我们只能近似地表示它
    • 增加二进制表示的长度可以提高表示的精度

2.4.2 IEEE浮点表示

  • IEEE 浮点标准用$V=(-1)^{s}\times M \times 2^{E}$E的形式来表示一个数:
    • 符号(sign) $s$决定这数是负数($s=1$)还是正数($s=0$)
      • 而对于数值0的符号位解释作为特殊情况处理。
    • 尾数(significand) $M$是一个二进制小数,它的范围是$[1, 2-\varepsilon]$或者是$[0, 1-\varepsilon ]$
    • 阶码(exponent) $E$的作用是对浮点数加权,这个权重是2的$E$次幂(可能是负数)。
  • 将浮点数的位表示划分为三个字段,分别对这些值进行编码:
    • 一个单独的符号位$s$直接编码符号$s$
    • $k$位的阶码字段exp=$e_{k-1}\cdots e_{1}e_{0}$编码阶码$E$
    • $n$位小数字段frac=$f_{n-1}\cdots f_{1}f_{0}$编码尾数$M$
      • 但是编码出来的值也依赖于阶码字段的值是否等于0。
        图2-32 标准浮点格式(浮点数由3 个字段表示。两种最常见的格式是它们
被封装到32 位(单精度)和64 位(双精度)的字中)
  • 给定位表示,根据exp 的值,被编码的值可以分成三种不同的情况(最后一种情况有两个变种)
    图2-33 单精度浮点数值的分类(阶码的值决定了这个数是规格化的、非规格化的或特殊值)
情况1: 规格化的值
  • 这是最普遍的情况。当exp的位模式既不全为0(数值0),也不全为1(单精度数值为255, 双精度数值为2047)时,都属于这类情况。
  • 阶码字段被解释为以偏置(biased) 形式表示的有符号整数
    • e.g $E=e-Bias$
    • 其中e 是无符号数,其位表示为$e_{k-1}\cdots e_{1}e_{0}$
    • Bias为一个$2^{k-1}-1$(单精度是127, 双精度是1023)的偏置值。
  • 小数字段frac被解释为描述小数值$f$,其中$0\le f<1$
    • 其二进制表示为$0.f_{n-1}\cdots f_{1}f_{0}$
  • 尾数定义为$M=1+f$
    • 这种方式也叫做隐含的以1开头的(implied leading 1)表示
      • 因为可以把$M$看成一个二进制表达式为$1.f_{n-1}\cdots f_{1}f_{0}$的数字。
情况2: 非规格化的值
  • 当阶码域为全0时,所表示的数是非规格化形式。
  • 阶码值是$E=1-Bias$
  • 尾数的值是$M=f$,也就是小数字段的值,不包含隐含的开头的1。
  • 非规格化数有两个用途
    1. 它们提供了一种表示数值0的方法
      • 符号位为0 => +0.0
      • 符号位为1 => -0.0
      • 根据IEEE的浮点格式,值+0.0 和-0.0 在某些方面被认为是不同的,而在其他方面是相同的
    2. 非规格化数的另外一个功能是表示那些非常接近于0的数
      • 它们提供了一种属性,称为逐渐溢出(gradual underflow) ,其中,可能的数值分布均勻地接近于0.0
情况3: 特殊值
  • 当指阶码全为1 的时候出现的
    • 当小数域全为0时,得到的值表示无穷
      • 当$s=0$时是$+\infty$
      • 当$s=1$时是$-\infty$
    • 当我们把两个非常大的数相乘,或者除以零时,无穷能够表示溢出的结果
  • 当小数域为非零时,结果值被称为“NaN”
    • 一些运算的结果不能是实数或无穷, 比如$\sqrt{-1}$或$\infty – \infty$
    • 在某些应用中,表示未初始化的数据时,它们也很有用处

2.4.3 数字示例

图 2-35 8 位浮点格式的非负值示例a=4 的阶码位的和 n 3 的小数位。偏置量是 7)
– 最大非规格化数$\frac{7}{512}$和最小规格化数$\frac{8}{512}$之间的平滑转变
– 这种平滑性归功于我们对非规格化数的$E$的定义。
– 通过将$E$定义为$1-Bias$而不是$-Bias$, 我们可以补偿非规格化数的尾数没有隐含的开头的1。

2.4.4 舍入

  • 舍入(rounding)运算的任务
    • 对于值x,我们一般想用一种系统的方法,能够找到“最接近的”匹配值x’,它可以用期望的浮点形式表示出来。
  • IEEE浮点格式定义了四种不同的舍入方式默认的方法是找到最接近的匹配,而其他三种可用于计算上界和下界。
    1. 向偶数舍入(round-to-even)
      • 也被称为向最接近的值舍入(round-to-nearest), 是默认的方式
      • 它将数字向上或者向下舍人,使得结果的最低有效数字是偶数
      • 在大多数现实情况中避免了统计偏差
        • 在50%的时间里,它将向上舍入,而在50%的时间里,它将向下舍入
    2. 向零舍入方式把正数向下舍入,把负数向上舍入
    3. 向下舍入方式把正数和负数都向下舍人
    4. 向上舍入方式把正数和负数都向上舍人
  • 我们将最低有效位的值0认为是偶数,值1认为是奇数。

2.4.5 浮点运算

  • IEEE 标准指定了一个简单的规则,来确定诸如加法和乘法这样的算术运算的结果:
    把浮点值x和y看成实数,
    而某个运算$\odot$定义在实数上
    计算将产生$Round(x\odot y)$

    • 这是对实际运算的精确结果进行舍入后的结果。
  • IEEE还制定一些规则规定含有特殊值的运算
    • 比如1/-0.0和1/+0.0
  • 对于浮点数加法$x+^{f}y$
    • 对于所有x和y的值,这个运算是可交换的
    • 这个运算是不可结合的
      • 比如(3.14+1e10)-1e10求值得到0.0——因为舍入,值3.14会丢失。
      • 3.14+(1e10-1e10)得出值3.14。
    • 另一方面,浮点加法满足了单调性属性
      • 若a≥b,那么对于任何a,b以及x的值,除了NaN,都有x+a≥x+b
  • 对于浮点数乘法$x*^{f}y$ – 定义为$Round(x\times y)$
    • 这个运算在乘法中是封闭的(虽然可能产生无穷大或NaN)
    • 可交换的
    • 乘法单位元为1.0
    • 不具有可结合性
      • 由于可能发生溢出,或者由于舍人而失去精度
    • 在加法上不具备分配性
    • 浮点乘法满足下列单调性:
      a≥b 且 c≥0 => $a*^{f}c \ge a*^{f}b$
      a≤b 且 c≤0 => $a*^{f}c \le a*^{f}b$
    • 还可以保证,只要a≠NaN,就有$a*^{f}a \ge 0$

2.4.6 C 语言中的浮点数

  • 当程序文件中出现下列句子时,编译器GCC会定义程序常数INFINITY(表示$+\infin$)和NAN(表示NaN):
    #define _GNU_SOURCE 1
    #include <math.h>
    
  • 当在int、 float 和double 格式之间进行强制类型转换时,程序改变数值和位模式的原则如下(假设int 是32 位的):
    • 从int转换成float 数字不会溢出,但是可能被舍入。
    • 从int或float转换成double能够保留精确的数值。
    • 从double 转换成floatÿ 因为范围要小一些,所以值可能溢出成$+\infin$或$-\infin$另外,由于精确度较小,它还可能被舍人。
    • 从float 或者double 转换成intÿ 值将会向零舍入
[深入理解计算机系统] 01 计算机系统漫游 笔记

[深入理解计算机系统] 01 计算机系统漫游 笔记

计算机 ComputerScience|计算机原理 ComputerSystem


@ZYX 写于2020年07月08日

第一章 计算机系统漫游

前言

  • 计算机系统是由硬件和系统软件组成的,它们共同工作来运行应用程序。

1.1 信息就是位+上下文

  • hello程序的生命周期是从一个源程序(或者说源文件)开始的 , 即程序员利用编辑器创 建并保存的文本文件
  • 源程序实际上就是一个由值0和1组成的位(bit) 序列
    • 8个位被组织成一组,称为字节
    • 每个字节表示程序中某个文本字符
      • 大部分系统都使用ASCII来记录字符
      • 这种方式实际上就是用一个单字节大小的整数值来表示每个字符
      • 文本文件:只由ASCII字符构成的文件
        • 二进制文件: 所有其他文件

1.2 程序被其他程序翻译成不同的格式

  • hello程序的生命周期
    1. 从一个高级C语言程序开始,这种形式能够被人读懂
    2. 为了在系统上运行hello.c,每条C语句都被转化为低级机器语言指令
    3. 这些指令按照称为可执行目标程序的格式打包,并以二进制磁盘文件的形式存放起来
      • 目标程序也称为可执行目标文件
  • 编译系统: 四个阶段的程序(预处理器编译器汇编器链接器

    1. 预处理阶段
      • 预处理器(cpp) 根据以字符#开头的命令,修改原始的C程序。
      • 结果就得到了另一个C程序,通常是以.1作为文件扩展名
    2. 编译阶段
      • 编译器(cel) 将文本文件hello.i翻译成文本文件hello.S,它包含一个汇编语言程序。
        • 汇编语言非常有用,因为它为不同高级语言的不同编译器提供了通用的输出语言
    3. 汇编阶段
      1. 汇编器(as) 将hello.s翻译成机器语言指令
      2. 把这些指令打包成一种叫 可重定位目标程序(relocatable object program) 的格式
      3. 并将结果保存在目标文件hello.o中。
    4. 链接阶段
      • 链接器(Id) 就负责处理这种合并。
      • 结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件)
        • 可以被加载到内存中,由系统执行。

1.3 了解编译系统如何工作是大有益处的

  • 有一些重要的原因促使程序员必须知道编译系统是如何工作的,其原因如下:
    1. 优化程序性能
      为了在C程序中做出好的编码选择,需要了解机器代码以及编译器将不同的C语句转化为机器代码的方式
    2. 理解链接时出现的错误
    3. 避免安全漏洞
      e.x 缓冲区溢出错误是造成大多数网络和服务器上安全漏洞的主要原因

1.4 处理器读并解释存储在存储器中的指令

  • 要想在Unix系统上运行可执行文件,我们将它的文件名输入到称为外壳(shell) 的应用程序中
unix> ./hello
hello, world
unix>
  • shell是一个命令行解释器
    1. 它输出一个提示符
    2. 等待你输入一个命令行
    3. 然后执行这个命令
    • 如果该命令行的第一个单词不是shell命令,外壳就会假设是可执行文件的名字,它将加载并运行这个文件

1.4.1 系统的硬件组成

  1. 总线
    • 贯穿整个系统的是一组电子管道,称做总线
    • 它携带信息字节并负责在各个部件间传递
    • 通常总线被设计成传送定长的字节块,也就是字(word)
      • 字中的字节数(即字长)是一个基本的系统参数
        • 有的是4个字节(32位),有 的是8个字节(64位)
  2. I/O设备
    • 输入/输出(I/O)设备是系统与外部世界的联系通道
    • 我们的示例系统包括4个I/O设备:
      1. 键盘:用户输入
      2. 鼠标:用户输入
      3. 显示器:用户输出
      4. 磁盘驱动器(简单地说就是磁盘): 长期存储数据程序
    • 每个I/O设备都通过一个控制器适配器与I/O总线相连
      • 控制器和适配器之间的区别主要在于封装方式:
        1. 控制器是置于I/O设备本身或者系统的主印制电路板(通常称为主板)上的芯片组
        2. 适配器是一块插在主板插槽上的
  3. 主存
    • 是一个临时存储设备
    • 在处理器执行程序时,存放:
      1.程序和程序处理的数据
    • 由一组动态随机存取存储器(DRAM)芯片组成的
      • 从逻辑上来说,存储器是一个线性字节数组
        • 每个字节都有其唯一的地址(即数组索引),这些地址是从零开始的
        • 一般来说,组成程序的每条机器指令都由不同数量的字节构成
        • 与C程序变量相对应的数据项大小是根据类型变化的
  4. 处理器
    • 中央处理单元(CPU),简称处理器,是解释(或执行)存储在主存中指令的引擎。
    • 核心是一个字长的存储设备(或寄存器),称为程序计数器(PC)。
      • 在任何时刻,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)
    • 从系统通电,到断电,处理器一直在
      1. 执行PC指向的指令,
      2. 再更新程序计数器,使其指向下一条指令。
    • 类似按照一个非常简单的指令执行模型来操作的
      • 这个模型是由指令集结构决定的
      • 指令按照严格的顺序执行,这里“执行”包含执行一系列步骤:
        1. 读取:处理器从PC指向的存储器处读取指令
        2. 解释:解释指令中的位
        3. 执行:执行指令指示的简单操作
        4. 更新:更新PC,使其指向下一条指令
          • 这条指令并不一定与存储器中刚刚执行的指令相邻。
            • 比如 if else之类的工作流
      • 操作是围绕着主存寄存器文件(register file)和算术/逻辑单元(ALU)进行的
        • 寄存器文件是一个小的存储设备
          • 由一些1字长的寄存器组成,每个寄存器都有唯一的名字
        • ALU计算新的数据地址值
    • 但是实际上现代处理器使用了非常复杂的机制来加速程序的执行
      • 因此可以这样区分处理器的指令集结构微体系结构
        • 指令集结构描述的是每条机器代码指令的效果
        • 微体系结构描述的是处理器实际上是如何实现的

1.4.2 运行hello程序

  • 运行hello程序时发生了什么:
    1. shell程序执行它的指令: 等待我们输入一个命令
    2. 输入 “./hello”并回车
    3. shell程序将字符逐一读入寄存器
    4. 最后把寄存器中的字符存放到存储器
      图片

1.5 高速缓存至关重要

  • 复制数据就是开销,减缓了程序“真正的”工作。因此,设计者的一个主要目标就是使这些复制操作尽可能快地完成
  • 根据机械原理,较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于同类的低速设备
    • 一个典型的寄存器文件只存储几百字节的信息,而主存里可存放几十亿字节。
    • 处理器从寄存器文件中读数据的速度比从主存中读取几乎要快100倍
  • 针对这种处理器与主存之间的差异,系统设计者釆用了更小、更快的存储设备,即高速缓存存储器(简称高速缓存)
    • 作为暂时的集结区域,用来存放处理器近期可能会需要的信息
    • L1和L2:
      容量 速度
      L1 数万字节 几乎和访问寄存器文件一样快
      L2 数十万到数百万字节 比访问L1的时间长5倍,比访问主存的时间快5〜10倍
      • L1和L2高速缓存是用一种叫做静态随机访问存储器(SRAM) 的硬件技术实现的
      • 比较新的、处理能力更强大的系统甚至有L3高速缓存
    • 系统可以获得一个很大的存储器,同时访问速度也很快,
      • 原因是利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势
      • 大部分的存储器操作都能在快速的高速缓存中完成。

1.6 存储设备形成层次结构

  • 在处理器和一个又大又慢的设备(例如主存)之间插入一个更小更快的存储设备(例如高速 缓存)的想法已经成为了一个普遍的观念
  • 每个计算机系统中的存储设备都被组织成了一个存储器层次结构
  • 从上至下,设备变得访问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜
    Figure 1.9 An example of a memory hierarchy.
  • 存储器层次结构的主要思想是一层上的存储器作为低一层存储器的高速缓存
  • 程序员同样可以利用对整个存储器层次结构的理解来提高程序性能

1.7 操作系统管理硬件

  • shell和hello程序都依靠操作系统提供的服务来访问键盘、显示器、磁盘或者主存
  • 操作系统是应用程序硬件之间插入的一层软件
    • 所有应用程序对硬件的操作尝试都必须通过操作系统。
  • 操作系统有两个基本功能:
    1. 防止硬件被失控的应用程序滥用
    2. 向应用程序提供简单一致的机制来控制复杂而又通常大相径庭的低级硬件设备
      Figure 1.10 Layered view of a computer system.
  • 操作系统通过几个基本的抽象概念进程、虚拟存储器和文件)来实现这两个功能
    • 文件是对I/O设备的抽象表示
    • 虚拟存储器是对主存磁盘I/O设备的抽象表示
    • 程则是对处理器主存I/O设备的抽象表示
      Figure 1.11 Abstractions provided by an operating system.

1.7.1 进程

  • 进程是操作系统对一个正在运行的程序的一种抽象。
  • 在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件
  • 并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的
  • 一个CPU看上去都像是在并发地执行多个进程
    • 这是通过处理器在进程间切换来实现的。
    • 操作系统实现这种交错执行的机制称为上下文切换
  • 上下文
    • 定义:操作系统保持跟踪进程运行所需的所有状态信息
    • 上下文切换:
      • 当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即:
        1. 保存当前进程的上下文
        2. 恢复新进程的上下文
        3. 然后将控制权传递到新进程。
  • 从一个进程到另一个进程的转换是由操作系统的内核kernel管理的
    • 内核是操作系统代码常驻主存的部分
    • 当应用程序需要操作系统的某些操作时,他就执行系统调用system call指令,将控制权传递给内核。然后内核执行被请求的操作返回应用程秀
      Figure 1.12 Process context switching.

1.7.2 线程

  • 在现代系统中,一个进程实际上可以由多个称为线程执行单元组成
    • 每个线程都运行在进程的上下文中,并共享同样的代码和全局数据

1.7.3 虚拟内存

  • 虚拟内存为每个进程提供了一个假象,即每个进程都在独占地使用主存
  • 每个进程看到的是一致的存储器,称为虚拟地址空间
    Figure 1.13 Process virtual address space.
    请注意,图中的地址是从下往上增大的

  • 在Linux中,地址空间
    • 最上面的区域是为操作系统中的代码和数据保留的,这对所有进程来说都是一样的。
    • 底部区域存放用户进程定义的代码和数据
  • 每个进程看到的虚拟地址空间由大量准确定义的构成,每个区都有专门的功能。
    1. 程序代码和数据:
      • 代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置
      • 代码和数据区是直接按照可执行目标文件的内容初始化的
    2. 堆:
      • 当调用如malloc和free这样的C标准库函数时,可以在运行时动态地扩展和收缩
    3. 共享库:
      • 大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样共享库的代码和数据的区域
    4. 栈:runtime stack
      • 编译器用它来实现函数调用
      • 用户栈在程序执行期间可以动态地扩展和收缩
    5. 内核虚拟存储器:
      • 内核总是驻留在内存中,是操作系统的一部分。地址空间顶部的区域是为内核保留的,
      • 不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。
      • 基本思想是把一个进程虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高速缓存

1.7.4 文件

  • 文件就是字节序列,仅此而已
  • 系统中的所有输入输出都是逋过使用一小组称为Unix-I/O的系统函数调用读写文件来实现的

1.8 系统之间利用网络通信

  • 实际上,现代系统经常通过网络和其他系统连接到一起。
  • 从一个单独的系统来看,网络可视为一个I/O设备

  • 随着Internet出现,将一台主机的信息复制到另外一台主机已经成为计算机系统最重要的用途之一

  • telnet

1.9 重要主题

总结一下我们旋风式的系统漫游: 系统是硬件和系统软件互相交织的集合体,它们必须共同协作以达到运行应用程序的最终目的

1.9.1 Amdahl 定律

  • 主要思想:
    • 当我们对系统的某个部分加速时,其对系统整体性能的影响取决于-该部分的重要性+加速程度
    • 参数:
      1. $T_{old}$ : 系统执行某应用程序的时间
      2. α : 该程序可提升部分所需时间的占比
      3. k :该部分可提升的比例
    • 可以推出:
      1. $alpha T_{old}$ :可提升部分所需时间
      2. $(alpha T_{old})/k$ :提升后,该部分所需时间
    • 提升后的整个程序所需时间:
      $T_{new} = (1-alpha)T_{old} + (alpha T_{old})/k = T_{old}[(1-alpha)+alpha /k]$
    • 加速比:
      $S=frac{1}{(1-alpha)+alpha/k}$
    • 推论
      • 要想显著加速整个系统,必须提升系统中相当大的部分的’速度

1.9.2 并发和并行

  • 有两个需求是驱动进步的持续动力:
    1. 一个是我们想要计算机做得更多,
    2. 另一个是我们想要计算机运行得更快
  • 并发(concurrency)是一个通用的概念,指一个同时具有多个活动的系统
  • 并行(parallelism)指的是用并发使一个系统运行得更快
    • 可以在系统的多个抽象层次上运用。(这里强调三个层次)
  1. 线程级并发
    • 构建在进程这个抽象之上,我们能够设计出同时执行多个程序的系统,这就导致了并发。
    • 单处理器系统
      • 即使处理器必须在多个任务间切换,大多数实际的计算也都是由一个处理器来完成的系统不
    • 多处理器系统
      • 一个由单操作系统内核控制的多处理器组成的系统
      • 随着多核处理器和超线程(hyperthreading)的出现,这种系统才变得常见。
      • 多核处理器是将多个CPU(称为“核”)集成到一个集成电路芯片上
        • 每个核都有自己的L1和L2高速缓存
        • 但是它们共享更高层次的高速缓存,以及到主存的接口。
      • 超线程有时称为同时多线程(simultaneous multi-threading)
        • 是一项允许一个CPU执行多个控制流的技术
        • 它涉及CPU某些硬件有多个备份:比如 PC和寄存器文件
        • 而其他的硬件部分只有一份,比如执行浮点算术运算的单元。
        • 超线程的处理器可以在单个周期的基础上决定要执行哪一个线程
          • 这使得CPU能够更好地利用它的处理资源
      • 多处理器的使用可以从两个方面提高系统性能:
        1. 它减少了在执行多个任务时模拟并发的需要
        2. 它可以使应用程序运行得更快
  2. 指令级并行
    • 在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行
    • 其实每条指令从开始到结束需要长得多的时间,大约20个或者更多的周期,但是是处理器使用了非常多的聪明技巧来同时处理多达100条的指令。
    • 如果处理器可以达到比一个周期一条指令更快的执行速率,就称之为超标量 (superscalar) 处理器
  3. 单指令、多数据并行
    • 允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。
    • 提供这些SIMD指令多是为了提高处理影像、声音和视频数据应用的执行速度

1.9.3 计算机系统中抽象的重要性

  • 抽象的使用是计算机科学中最为重要的概念之一
已到首页—已到末页