深入理解计算机系统(CSAPP) 第二章

家庭作业

2.57

借助 C++ 模板可以很方便的实现。

// g++ -o main main.cc -std=c++11
#include <string>
#include <iostream>

template<typename T>
void show_bytes(T t) {
  // 获取字节数量
  size_t byte_count = sizeof(t);
  std::string bit_str;
  // 从前向后遍历
  for (size_t i = 0; i < byte_count; i++) {
  	// 取出 t 的第 i 个字节的地址
    const uint8_t *ptr = reinterpret_cast<const uint8_t *>(&t) + i;
    // 依次取出第 i 个字节的 8 个比特。
    for (uint8_t j = 0, bit = 128; j < 8; j++, bit >>= 1) {
      if ((*ptr) & bit) {
        bit_str += '1';
      } else {
        bit_str += '0';
      }
    }
  }
  std::cout << "type: " << typeid(t).name() << ", val: " << t << ", bit: " << bit_str << std::endl;
}

int main() {
  show_bytes(short(100));
  show_bytes(int(100));
  show_bytes(long(100));
  show_bytes(float(100.00));
  show_bytes(double(100.00));
  return 0;
}

2.58

借助 union 可以很容易实现。在下述代码中:

  • 如果是大端机器,则 ui8[0] = 0x01, ui8[1] = 0x02。
  • 如果是大端机器,则 ui8[0] = 0x02, ui8[1] = 0x01。
bool is_little_endian() {
  union Layout {
    uint16_t ui16;
    uint8_t ui8[2];
  };
  Layout layout;
  layout.ui16 = 0x0102;
  return layout.ui8[0] == 0x02;
}

2.59

// x = 0x89ABCDEF
// y = 0x76543210
// z = 0x765432EF
z = (x&0xFF)|(y&0xFFFFFF00); 

2.60

#include <stdio.h>
#include <stdint.h>

unsigned replace_byte(unsigned x, int i, uint8_t b) {
  unsigned mask = 0xFF << (i<<3);
  return (x&(~mask))|(uint32_t(b)<<(i<<3));
}

int main() {
  printf("%x\n", replace_byte(0x12345678, 2, 0xAB));
  printf("%x\n", replace_byte(0x12345678, 0, 0xAB));
  return 0;
}

2.61

X 的任何位都等于 1

!(~x)

X 的任何位都等于 0

!x

X 的最低有效字节中的位都等于 1

!uint8_t(~x)

X 的最高有效字节中的位都等于 1

!(((unsigned int)(~x)>>24))

2.62

#include <stdio.h>
#include <stdint.h>

int int_shifts_are_arithmetic() {
  int8_t x = 0xFF;
  return !(~(x>>1));
}

int main() {
  printf("%d\n", int_shifts_are_arithmetic());
  return 0;
}

2.63

unsigned srl(unsigned x, int k) {
  unsigned xsra = (int) x >> k;

  int w = 8 * sizeof(x);

  // 将掩码的最高的 k 个比特置为 0,其余置为 1,以消除符号位可能给 xsra 带来的影响
  xsra &= ~(((1<<k)-1)<<(w-k));

  return xsra;
}

int sra(int x, int k) {
  int xsrl = (unsigned) x >> k;

  int w = 8 * sizeof(x);

  if (x & (1<<(w-1))) {
    // x 是负数,将最高的 k 个比特置为 1
    xsrl |= ((1<<k)-1) << (w-k);
  }

  return xsrl;
}

2.64

怎么定义奇数位呢?不妨从最低位开始编号吧,第一位,第二位,······, 依次类推。

int any_odd_one(uint32_t x) {
  return !(~((x & 0x55555555) | 0xAAAAAAAA));
}

2.65

int odd_ones(unsigned x) {
  int flag = 0;
  while (x) {
    flag += (x&1);
    x >>= 1;
  }
  return flag & 1;
}

2.66

int leftmost_one(uint32_t x) {
  uint32_t mask = 0x80000000;

  while ((x & mask) == 0 && mask) {
    mask >>= 1;
  }
  return mask;
}

2.67

A: 32位机器最多只能移位31位,否则位的值未定义。
B:

int bad_int_size_is_32() {
  int set_msb = 1<<31;
  int beyond_msb = 2<<31;

  return set_msb && !beyond_msb;
}

C:

int bad_int_size_is_32() {
  int set_msb = 1;
  for (int i = 1; i <= 31; i++) {
    set_msb <<= 1;
  }
  int beyond_msb = set_msb<<1;

  return set_msb && !beyond_msb;
}

2.68

int lower_one_mask(int n) {
  int mask = 0;
  for (int i = 0; i < n; i++) {
    (mask <<= 1) |= 0x1;
  }
  return mask;
}

2.69

uint32_t rotate_left(uint32_t x, int n) {
  if((n &= 0x1F) == 0) {
    return x;
  }
  uint32_t mask = (1<<(32-n))-1;
  uint32_t suf = x & mask;
  uint32_t pre = x & (~mask);
  return (suf << n) | (pre >> (32-n));
}

2.70

int fits_bits(int x, int n) {
  int bit_count = sizeof(x)*8;
  unsigned int sign_mask = 1<<(bit_count-1);
  unsigned int cursor_mask = sign_mask >> 1;
  int count = 0;
  while ((x&cursor_mask) == (x&sign_mask) && sign_mask > 1) {
    count++;
    sign_mask >>= 1;
  }
  return bit_count - count <= n;
}

2.71

错误有两处:

  • 符号位丢失。
  • 右移多了8个比特。

我的实现:

typedef unsigned packed_t;

int xbyte(packed_t word, int bytenum) {
  return int8_t(word >> ((bytenum-1)<<3) & 0xFF);
}

2.72

一处错误:
当二目运算符两侧的整数符号不一致时,会都被隐式的转换为无符号整数。众所周知,两个无符号数做减法,结果恒不为负。

我的代码:

void copy_int(int val, void *buf, int maxbytes) {
  if (maxbytes >= sizeof(val)) {
    memcpy(buf, (void *)&val, sizeof(val));
  }
}

2.73

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>

int saturating_add(int x, int y) {
  if (y < 0) {
    // 此时只可能发生负溢出
    if (x + y > x) {
      return INT_MIN;
    }
  } else {
    // 此时只可能发生正溢出
    if (x + y < x) {
      return INT_MAX;
    }
  }

  return x + y;
}


int main() {
  printf("%x\n", saturating_add(0, 1));
  printf("%x\n", saturating_add(0xFFFFFFFF, 1));
  printf("%x\n", saturating_add(0xFFFFFFFF, 0x8FFFFFFF));
  printf("%x\n", saturating_add(0x80000000, -1));
  printf("%x\n", saturating_add(0x7FFFFFFF, 1));
  printf("%x\n", saturating_add(0x7FFFFFFF, 0));
  printf("%x\n", saturating_add(0x80000000, 0));
  return 0;
}

2.74

int tsub_ok(int x, int y) {
  if (y >= 0) {
  	// 此时只可能负溢出,若负溢出则必然大于 x
    return x - y <= x;
  }
  // 此时只可能正溢出,若正溢出则 x - y 必然小于 x
  return x - y >= x;
}

2.75

这是一种 signed_high_prod(int, int) 的实现:

int signed_high_prod(int x, int y) {
  // 先转成 int64_t 并计算乘积
  int64_t m = int64_t(x) * int64_t(y);

  // 为了忽略符号位对右移的影响,先转为 uint64_t,再右移 32 位。
  uint32_t h = uint64_t(m) >> 32;

  // 转为 int32_t 并返回
  return int32_t(h);
}

设有如下两个变量:

uint32_t ux, uy;

接下来分三种情形讨论:

1.当 uxuxuxuyuyuy 的最高位为 0 时,传入 signed_high_prod 显然不会有任何问题。

2.当 uxuxuxuyuyuy 有且有一个的最高位是 1 时,不妨设 uxuxux 的最高位为 1,此时会有一个问题:uint64_t(x) 的高 32 位会是 0,但 int64_t(int32_t(ux)) 的高 32 位是 1。

为了解决这个问题,引入 px=ux−231px = ux - 2^{31}px=ux231,则 ux∗uyux*uyuxuy 可表示为:
(px+231)∗uy=px∗uy+231∗uy(px + 2^{31})*uy=px*uy+2^{31}*uy (px+231)uy=pxuy+231uy

px∗uypx*uypxuy 可通过 signed_high_prod(px, uy) 求得,231∗uy2^{31}*uy231uy 可通过简单的位运算获得。

3.当 uxuxuxuyuyuy 的最高位均为 1 时,引入 px=ux−231px = ux-2^{31}px=ux231py=uy−231py = uy-2^{31}py=uy231,则 ux∗uyux * uyuxuy 可表示为
(px+231)∗(py+231)=px∗py+(px+py)∗231+262 (px+2^{31})*(py+2^{31}) = px*py + (px+py)*2^{31} + 2^{62} (px+231)(py+231)=pxpy+(px+py)231+262

完全的代码如下:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <random>
#include <limits>
#include <iostream>

int signed_high_prod(int x, int y) {
  // 先转成 int64_t 并计算乘积
  int64_t m = int64_t(x) * int64_t(y);

  // 为了忽略符号位对右移的影响,先转为 uint64_t,再右移 32 位。
  uint32_t h = uint64_t(m) >> 32;

  // 转为 int32_t 并返回
  return int32_t(h);
}

uint32_t unsigned_high_prod(uint32_t ux, uint32_t uy) {
  // ux 和 uy 的最高位都是 1
  if ((ux & 0x80000000) && (uy & 0x80000000)) {
    uint32_t px = ux - 0x80000000;
    uint32_t py = uy - 0x80000000;

    uint32_t v = px + py;
    if ((px*py)&0x80000000) {
      v += 1;
    }
    return uint32_t(signed_high_prod(px, py)) + (v>>1) + uint32_t(1<<30);
  }

  if (ux < uy) {
    std::swap(ux, uy);
  }

  // 有且仅有一个数字的最高位是 1
  if (ux & 0x80000000) {
    uint32_t px = ux - 0x80000000;
    uint32_t v = ((uy&1) && (px*uy) & 0x80000000) ? 1 : 0;
    return uint32_t(signed_high_prod(px, uy)) + (uy>>1) + v;
  }

  // 最高位都是 0
  return uint32_t(signed_high_prod(ux, uy));
}

int main() {
  std::random_device rd;
  std::mt19937 gen{rd()};
  std::uniform_int_distribution<uint32_t> distrib(0, std::numeric_limits<uint32_t>::max());

  for (int i = 0; i < 1000000; i++) {
    uint32_t a = distrib(gen), b = distrib(gen);
    assert(unsigned_high_prod(a, b) == uint32_t(a*1UL*b>>32));
    // std::cout << a << ", " << b << std::endl;
  }

  return 0;
}

2.76

个人认为这道题主要在考察乘法溢出的检测方法以及错误的处理方式。

乘法溢出可用除法来检测。

我知道的错误处理有三种:

  1. 断言,简单粗暴直接导致进程停止;
  2. errno,通过错误码传递信息,让用户自行处理。
  3. 抛异常,这种 C 是不支持的。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

void *cmalloc(size_t nmemb, size_t size) {
  if (nmemb == 0 || size == 0) {
    return NULL;
  }

  // 检测乘法溢出
  size_t total = nmemb * size;
  if (total / nmemb != size) {
    // 设置 errno
    errno = ERANGE;
    return NULL;
  }

  void *result = malloc(total);
  if (result != NULL) {
    memset(result, 0, total);
  }

  return result;
}

2.77

A:17,二进制位 1001,可表示为 (x<<4) + x
B:-7,可表示为x-8*x,即 x-(x<<8)
C:60,可表示为x*64 - x*4,即 (x<<6) - (x<<2)
D:-112,可表示为x*16-x*128,即(x<<4)-(x<<8)

2.78

整数的除法都是向 0 取整。

常规写法可以是:

int divide_power2(int x, int k) {
	return x / (1<<k);
}

但题目要求符合位级整数编码规则,只能用算术右移来代替除法,但须注意向 0 舍入的问题。

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

int divide_power2(int x, int k) {
  // 先按 x 是正数处理;
  int result = x >> k;
  // x 是负数,再算一次。证明过程见原书 2.3.7。
  (x & INT_MIN) && (result = (x + (1<<k) - 1) >> k);
  return result;
}

2.79

该题接受乘法溢出,因此直接用左移和加法代替乘法,并复用上一题中的代码实现除法。

int mul3div3(int x) {
	return divide_power2((x<<1)+x, 2);
}

2.80

不接受溢出,可以简化为两次算术右移和一次加法:
x∗3/4=(x∗2+x)/4=x2+x4 x*3/4 = (x*2+x)/4 = \frac{x}{2} +\frac{x}{4} x3/4=(x2+x)/4=2x+4x

先除再加绝对不会溢出了,但要考虑向零舍入的问题。因为两次除法都做了舍入,这两部分加起来可能会大于等于 1。

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <assert.h>

int threefourths(int x) {
  // 复用上一题的代码
  int result = divide_power2(x, 1) + divide_power2(x, 2);

  // 当 x 是正数时,最低两位均为 1 时需补一
  (!(x & INT_MIN)) && (x & 0x3) == 0x3 && (result += 1);

  // 当 x 是负数时,最低位是 01 时,两次除法会比先乘后除多进一次 bias,需减去
  (x & INT_MIN) && (x & 0x3) == 0x1 && (result -= 1);

  return result;
}

2.81

A:1w−k0k1^{w-k}0^k1wk0k

unsigned int mask = -1;
mask ^= (1<<k)-1;

B:0w−j−k1k0j0^{w-j-k}1^k0^j0wjk1k0j

unsigned int mask = 0;
mask ^= ((1<<(k+j))-1) ^ ((1<<j)-1);

2.82

A:(x < y) == (-x > -y)
有反例。

x = INT_MINy = INT_MIN+1 时,上式为 0。

B:((x+y)<<4)+y-x == 17*y+15*x

恒成立。

  1. 左移 4 位等价于乘 16。
  2. 加减乘都是在模 2322^{32}232 意义下的,所以无需担心溢出带来的差异。

C:~x+~y+1==~(x+y)
恒成立。

将上式中的 ~ 去掉,可得 (-x-1)+(-y-1)+1==-(x+y)+1,显然两边恒等。

D:(ux-uy)==-(unsigned)(y-x)

恒成立。在模 2322^{32}232 下的加、减、取反的运算结果的二进制表示不会受有无符号的影响。

E:((x>>2)<<2)<=x

恒成立。左边的运算会将最低位的两个比特置零,其值必然会减小。

2.83

A:答案为 Y2k−1\frac{Y}{2^k-1}2k1Y,推导过程如下:

题干中所示的无穷串可表示为 Y2k+Y22k+Y23k+⋅⋅⋅⋅\frac{Y}{2^k} + \frac{Y}{2^{2k}} + \frac{Y}{2^{3k}} +····2kY+22kY+23kY+,代入等比序列求和公式可得:Y2k−1−Y2nk∗(2k−1)\frac{Y}{2^k-1} - \frac{Y}{2^{nk}*(2^k-1)}2k1Y2nk(2k1)Y,当 nnn 趋于无穷大时易得其值为 Y2k−1\frac{Y}{2^k-1}2k1Y

B:代入上式可得 57\frac{5}{7}75615\frac{6}{15}1561963\frac{19}{63}6319

2.84

浮点数的大小本可以直接比较二进制,但这题要求 -0+0 相等,那就要特殊处理下了。

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <assert.h>

unsigned f2u(float x) {
  unsigned ux = 0;
  memcpy(&ux, &x, sizeof(x));
  return ux;
}

int float_le(float x, float y) {
  printf("%f, %f\n", x, y);
  unsigned ux = f2u(x);
  unsigned uy = f2u(y);

  unsigned sx = ux >> 31;
  unsigned sy = uy >> 31;

  //      判断 ux 和 uy 是否为零;                                      其他情形正常比较即可;
  return (((ux & 0x7FFFFFFF) == 0) && ((uy & 0x7FFFFFFF) == 0) && 1) || (ux <= uy);
}

2.85

bias=2k−1−1bias = 2^{k-1}-1bias=2k11

非规格化的 EEEE=1−biasE = 1-biasE=1bias
规格化的 EEEE=e−biasE = e - biasE=ebias

非规格化的 MMMM=fM = fM=f
规格化的 MMMM=1+fM = 1+fM=1+f

V=M∗2EV = M*2^EV=M2E

A. 数 7.0
指数域最高位和最低位为 1,其余为 0。
尾数域最高两位为 1,其余为 0。

  • f=1100...f = 1100...f=1100...
  • M=1.1100....M = 1.1100....M=1.1100....
  • e=10...01e = 10...01e=10...01
  • E=00....10E = 00....10E=00....10
  • V=111.0000=7.0V = 111.0000 = 7.0V=111.0000=7.0

B. 能够被准确描述的最大奇整数

m=min⁡(n,2k−2−bias)m=\min(n, 2^{k}-2 - bias)m=min(n,2k2bias)2k−2−bias2^{k}-2 - bias2k2bias 即最大的规格化的 EEE

尾数域最高的 mmm 位为1 ,其余为 0。

指数域的值即为 m+biasm + biasm+bias

  • f=1...0...f = 1...0...f=1...0... m 个 1, n - m 个 0
  • M=1.1...0...M = 1.1...0...M=1.1...0... 总共 m+1 个 1
  • e=m+biase = m + biase=m+bias
  • E=mE = mE=m
  • V=M∗2E=1.11....∗2m=111...=2m+1−1V = M* 2^E = 1.11....*2^{m} = 111... = 2^{m+1}-1V=M2E=1.11....2m=111...=2m+11

C. 最小的规格化数的倒数
先来看最小的规格化数,仅指数域最低位为 1,其余为 0,其值为 122k−1−2\frac{1}{2^{2^{k-1}-2}}22k121

易得倒数 22k−1−22^{2^{k-1}-2}22k12,易得 e=2k−1−2+bias=2k−3e = 2^{k-1}-2 + bias = 2^k-3e=2k12+bias=2k3

  • f=000...f = 000...f=000... 全为 0
  • M=1.000....M = 1.000....M=1.000....
  • e=2k−3e = 2^k-3e=2k3
  • E=2k−3−bias=2k−1−2=bias−1E = 2^k-3 - bias = 2^{k-1}-2 = bias-1E=2k3bias=2k12=bias1
  • V=2bias−1V = 2^{bias-1}V=2bias1

2.86

符号位阶码位整数位小数位十进制
最小的正非规格化数00…000…121−(214−1)−632^{1-(2^{14}-1)-63}21(2141)63
最小的正规格化数00…110…021−(214−1)2^{1-(2^{14}-1)}21(2141)
最大的正规格化数01…011…12214−1∗(2−2−63)2^{2^{14}-1}*(2-2^{-63})22141(2263)

2.87

HexMEVD
-080000-14-0-0.0
最小的大于2值400110251024\frac{1025}{1024}1024102511025512\frac{1025}{512}51210252.001953
512600019512512.0
最大的非规格化数03FF10231024\frac{1023}{1024}10241023-141023224\frac{1023}{2^{24}}22410230.000061
−∞-∞FC00--−∞-∞-inf
十六进制表示为 3BB0 的数3BB012364\frac{123}{64}64123-1123128\frac{123}{128}1281230.960938

2.88

A 的位A 的值B 的位B 的值
1 01110 001−916-\frac{9}{16}1691 0110 0010−916-\frac{9}{16}169
0 10110 10113∗2413*2^413240 1110 101013∗2413*2^41324
1 00111 110−7210-\frac{7}{2^{10}}21071 0000 0111−7210-\frac{7}{2^{10}}2107
0 00000 1015211\frac{5}{2^{11}}21150 0000 00015210\frac{5}{2^{10}}2105
1 11011 000−212-2^{12}2121 1110 1111−31∗23-31*2^33123
0 11000 1003∗283*2^83280 1111 0000+∞+∞+

2.89

A: 恒成立,int → double → float 和 int → float 的精度损失是一样的,都是发生在转 float 的时候。
B: 不成立。比如 y = INT_MAX 或者 x = INT_MIN,后者会发生溢出。
C: 不成立。比如 dx = 10, dz = -10, dy 是最大规格化数。
D: 不成立。比如 dx = 2,dz = 0.01,dy 是最大规格化数。
E: 不成立。比如 dx = 1,dz = 0。

2.90

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <assert.h>

float valid(int x) {
  float f = 1;
  if (x < 0) {
    for (; x; x++) {
      f /= 2.0;
    }
  } else {
    for (; x; x--) {
      f *= 2.0;
    }
  }
  return f;
}

float fpwr2(int x) {
  unsigned exp, frac;
  unsigned u;

  if (x < -149) {
    // float 能表示的最小正数是 s = 0,e = 0 (8 个 0), f = 0...1 (22 个 0,1 个 1)
    // 此时 E = 1 - bias = -126,M = 2^(-23),V = 2^(-149)。
    // 因此 当 x < -149 时无法表示了。
    exp = 0;
    frac = 0;
  } else if (x < -126) {
    // 非规格化的 float 能表示的最大的 2^x 是 s = 0, e = 0 (8 个 0),f = 10...0 ( 1 个 1,22 个 0)
    // 此时 E = 1 - bias = -126,M = 2^(-1),V = 2^(-127)。
    // 因此当 x < -126 可用非规格化表示
    exp = 0;
    frac = 1 << (149 + x);
  } else if (x < 128) {
    // 规格化的 float 能表示的最大的 2^x 是 s = 0,e = 1..10 (7 个 1,1 个 0),f = 0..0 (23 个 0)
    // 此时 E = 254 - 127 = 127,M = 1,V = 2^(127)
    // 因此当 x < 128 时可用规格化表示
    exp = x + 127;
    frac = 0;
  } else {
    // 表示无穷大
    exp = 255;
    frac = 0;
  }

  u = exp << 23 | frac;

  // return u2f(u);
  return *(float *)&u;
}

int main() {
  // 验证
  for (int i = -151; i <= 129; i++) {
    printf("x=%d, v=%.180f\n", i, valid(i));
    printf("x=%d, f=%.180f\n", i, fpwr2(i));
    printf("\n");
  }
  return 0;
}

2.91

A
0x40490FDB 对应的二进制是:

  • 符号位:0
  • 指数位:10000000,E = 128 - 126 = 2
  • 尾数位:100 1001 0000 1111 1101 1011
  • 二进制小数: 11.00 1001 0000 1111 1101 1011

B
227=Y2k−1\frac{22}{7} = \frac{Y}{2^k-1}722=2k1YY=22=14+7+1Y = 22 = 14 + 7 + 1Y=22=14+7+1k=log⁡28=3k = \log_{2}{8} = 3k=log28=3

二进制小数为 11.001 001 001 …

C
针对这道题,写了一个简单的转换工具,易得在第九位开始不一样了。

  • 1071\frac{10}{71}7110:0.0010010000
  • 17\frac{1}{7}71:0.0010010010
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <assert.h>
#include <math.h>

// 将 a/b 转成二进制
void f2b(uint32_t a, uint32_t b) {
  uint64_t x = 2;
  printf("0.");
  for (uint32_t i = 0; i < 10; i++, x *= 2) {
    if (a*x >= b) {
      uint64_t new_a = a*x-b;
      uint64_t new_b = b*x;
      a = new_a;
      b = new_b;
      printf("1");
    } else {
      printf("0");
    }
  }
  puts("");
}

int main() {
  f2b(10, 71);
  f2b(1, 7);
  return 0;
}

2.92

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <assert.h>
#include <math.h>

typedef uint32_t float_bits;

float_bits float_negate(float_bits f) {
  float_bits exp = f >> 23 & 0xFF;
  float_bits frac = f & 0x7FFFFF;

  // f is not a NaN
  if (!(exp == 0xFF && frac)) {
    // 符号位取反
    f ^= 0x80000000;
  }

  return f;
}

// float → float_btis
float_bits f2fb(float f) {
  return *(float_bits *) &f;
}

// float_btis → float
float fb2f(float_bits fb) {
  return *(float *) &fb;
}

int main () {
  for (uint64_t i = 0; i <= 0xFFFFFFFF; i++) {
    float f = fb2f(i);
    if (isnan(f)) {
      assert(isnan(fb2f(float_negate(i))));
    } else {
      assert(-f == fb2f(float_negate(i)));
    }
  }

  return 0;
}

2.93

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <assert.h>
#include <math.h>

typedef uint32_t float_bits;

float_bits float_absval(float_bits f) {
  float_bits exp = f >> 23 & 0xFF;
  float_bits frac = f & 0x7FFFFF;

  // f is not a NaN
  if (!(exp == 0xFF && frac)) {
    // 符号位置零
    f &= 0x7FFFFFFF;
  }

  return f;
}

float_bits f2fb(float f) {
  return *(float_bits *) &f;
}

float fb2f(float_bits fb) {
  return *(float *) &fb;
}

int main () {
  for (uint64_t i = 0; i <= 0xFFFFFFFF; i++) {
    float f = fb2f(i);
    if (isnan(f)) {
      assert(isnan(fb2f(float_absval(i))));
    } else {
      assert(fabs(f) == fb2f(float_absval(i)));
    }
  }

  return 0;
}

2.94

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <assert.h>
#include <math.h>

typedef uint32_t float_bits;

float_bits float_twice(float_bits f) {
  float_bits sign = f >> 31;
  float_bits exp = f >> 23 & 0xFF;
  float_bits frac = f & 0x7FFFFF;

  // 非规格化
  if (exp == 0) {
    // frac 最高位是 1,再乘 2 会变为规格化
    if (frac & 0x400000) {
      exp = 1;
    }

    frac <<= 1;

    // & 操作是为了消除 frac 左移带来的`溢出`。
    // 实际上这里不 & 也可以,溢出的比特正好和 exp 的最低位一致。
    return (sign << 31) | (exp << 23) | (frac & 0x7FFFFF);
  }

  // 规格化
  if (exp != 0xFF) {
    // 尾数隐含的以 1 作为开头,无法再左移 frac 了,只能递增 exp 一次。
    ++exp;
    if (exp == 0xFF) {
      // 无穷大了,frac 置为 0。
      frac = 0;
    }

    return (sign << 31) | (exp << 23) | frac;
  }

  // f 是 +oo 或者 -oo
  // f 是 NaN
  return f;
}

float_bits f2fb(float f) {
  return *(float_bits *) &f;
}

float fb2f(float_bits fb) {
  return *(float *) &fb;
}

int main () {
  for (uint64_t i = 0; i <= 0xFFFFFFFF; i++) {
    float f = fb2f(i);
    if (isnan(f)) {
      assert(isnan(fb2f(float_twice(i))));
    } else {
      assert(f*2 == fb2f(float_twice(i)));
    }
  }

  return 0;
}

2.95

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <assert.h>
#include <math.h>

typedef uint32_t float_bits;

float_bits float_half(float_bits f) {
  float_bits sign = f >> 31;
  float_bits exp = f >> 23 & 0xFF;
  float_bits frac = f & 0x7FFFFF;

  // 非规格化
  if (exp == 0) {
    float_bits suf = frac & 0x3;
    frac >>= 1;

    // 向偶数取零,舍入方式见原书 84 页
    if (suf == 0x3) {
      frac ++;
    }

    return (sign << 31) | (exp << 23) | frac;
  }

  // 规格化
  if (exp != 0xFF) {
    // 尾数隐含的以 1 作为开头,无法再右移 frac 了,只能递减 exp 一次。
    --exp;
    if (exp == 0) {
      float_bits suf = frac & 0x3;

      // 变成非规格化了,隐藏 1 需要插入 frac 的最高位。
      frac >>= 1;
      frac |= 0x400000;

      // 向偶数取零,舍入方式见原书 84 页
      if (suf == 0x3) {
        frac ++;
      }
    }

    return (sign << 31) | (exp << 23) | frac;
  }

  // f 是 +oo 或者 -oo
  // f 是 NaN
  return f;
}

float_bits f2fb(float f) {
  return *(float_bits *) &f;
}

float fb2f(float_bits fb) {
  return *(float *) &fb;
}

int main () {
  for (uint64_t i = 0; i <= 0xFFFFFFFF; i++) {
    float f = fb2f(i);
    if (isnan(f)) {
      assert(isnan(fb2f(float_half(i))));
    } else {
      assert(f/2 == fb2f(float_half(i)));
    }
  }
  return 0;
}

2.96

总结一下规则:

  • 如果是 NaN 或者超出范围,则返回 INT_MIN。
  • 其他情况向 0 舍入。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <assert.h>
#include <math.h>

typedef uint32_t float_bits;

int32_t float_f2i(float_bits f) {
  float_bits sign = f >> 31;
  float_bits exp = (f >> 23) & 0xFF;
  float_bits frac = f & 0x7FFFFF;
  float_bits bias = 0x7F;

  // printf("sign=%x, exp=%x, frac=%x\n", sign, exp, frac);

  // 非规格化数,必然小于 1,向 0 舍入即为 0
  if (exp == 0) {
    return 0;
  }

  // 规格化数
  if (exp != 0xFF) {
    // E = exp - bias < 0,必然小于 1,向 0 舍入即为 0
    int32_t E = exp - bias;
    if (E < 0) {
      return 0;
    }

    // 拼接上隐藏位,M 可能有 24 个 1
    float_bits M = frac | 0x800000;

    // 形如 1.x..y 的二进制小数,最多只能左移 30 次。
    if (E <= 30) {
      if (E > 23) {
        M <<= (E-23);
      } else {
        M >>= (23-E);
      }
      if (sign) {
        M *= -1;
      }
      return M;
    }

    // 溢出了
    return INT_MIN;
  }

  // NaN、+oo、-oo
  return INT_MIN;
}


float_bits f2fb(float f) {
  return *(float_bits *) &f;
}

float fb2f(float_bits fb) {
  return *(float *) &fb;
}

int main () {
  for (uint64_t x = 0; x <= 0xFFFFFFFF; x++) {
    float f = fb2f(x);
    int i = float_f2i(f2fb(f));

    if (isnan(f) || isinf(f)) {
      if (i != INT_MIN) {
        printf("float=%.120f, int=%llx, %x, %x\n", f, x, (int32_t)f, float_f2i(f2fb(f)));
        assert(0);
      }
    } else {
      if (((int32_t)f) != i) {
        printf("float=%.120f, int=%llx, %x, %x\n", f, x, (int32_t)f, float_f2i(f2fb(f)));
        assert(0);
      }
    }
  }
  return 0;
}

2.97

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <assert.h>
#include <math.h>

typedef uint32_t float_bits;

float_bits float_i2f(int32_t i) {
  float_bits sig = i & 0x80000000;
  float_bits abs = sig ? ((~i) + 1) : i;
  float_bits bais = 0x7F;

  if (abs == 0) {
    return 0x0;
  }

  // 必为规格化数了,最多保留24位(frac 23 个,隐藏位 1 个)

  // 左移使得最高位为 1
  float_bits E = 31;
  while (abs < 0x80000000) {
    abs <<= 1;
    --E;
  }

  // 舍弃隐藏位
  abs <<= 1;

  // 先保留低 9 位的值,舍入会用到
  float_bits bits_9 = abs & 0x1FF;

  // 右移 9 位以保留 23 个有效位
  abs >>= 9;

  if (bits_9 == 0x100) {
    // 在正中间,需保证舍入后最后一位是 0
    if (abs & 0x1) {
      ++abs;
    }
  } else if (bits_9 > 0x100) {
    // 在上半部分,只能向上舍入了
    ++abs;
  }
  // 1.1..1 + 0.0..01 = 10.0...0,
  if (abs == 0x800000) {
    abs = 0;
    ++E;
  }

  float_bits exp = E + 0x7F;

  return sig | (exp << 23) | abs;
}

float_bits f2fb(float f) {
  return *(float_bits *) &f;
}

float fb2f(float_bits fb) {
  return *(float *) &fb;
}

int main () {
  for (uint64_t i = 0; i <= 0xFFFFFFFF; i++) {
    float f0 = (int)i;
    float f1 = fb2f(float_i2f(i));
    if (f0 != f1) {
      printf("i=%llx, %lld\nf0=%.120f\nf1=%.120f\n", i, i, f0, f1);
      assert(0);
    }
  }

  return 0;
}

学习笔记

相对于人类更易使用的十进制,计算机使用二进制可以工作的更好。

本章主要介绍基于二进制的,整数和实数的编码方式、运算方式。

需要注意的是,计算机只能有有限的位对一个数字进行编码,当数字过大时其值会造成溢出。

2.1 信息存储

字节,由八个比特组成,是计算机中中最小的可寻址的内存单元。内存中的每个字节都由一个唯一的数字标识,这个数字成为该字节的地址。所有可能地址的集合称为虚拟地址空间。

2.1.1 十六进制表示法

在十六进制表示法中,用 ‘0’ ~ ‘9’ 以及 ‘A’ ~ ‘F’ 表示十六个值,分别对应 0 到 15。

在 C/C++ 中,以 ‘0x’ 或 ‘0X’ 开头的数字常量会被当做十六进制数字处理。

2.1.2 字数据大小

不同类型的变量占用可能不同。

相同类型的变量用不同的方式编译(gcc -m32 or gcc -m64),占用字节数也可能不同。

“32 位程序” 或 “64 位程序” 指的是编译方式,而非其运行的机器类型。

大多数的 64 位机器可以运行 32 位的程序,这是一种向后兼容。显然,32 位机器可以运行 64 位的程序是不行的。

2.1.3 寻址和字节顺序

对象的地址:用对象所使用字节中最小的地址。

字节顺序:

  • 大端,高位在大地址,低位在小地址。
  • 小端,高位在小地址,低位在大地址。

一个容易记忆的技巧 —— 「大同小异」—— 当地址从左到右增加时,大端和人类的阅读习惯相『同』,小端则相『异』。

当两台机器通过网络通信时,需要注意字节顺序的差异,一般方法是都按约定好的网络顺序收发数据。

2.1.4 表示字符串

字符串的值不会受大小端的影响,我理解这是因为 char 是单字节的。反过来想,大小端只会影响多字节的对象。

2.1.5 表示代码

完全相同的代码在不同平台上编译得到的二进制文件也是完全不同。因此代码能移植是指相同的代码可在不同的平台上编译并得到执行过程相同的二进制文件,但并不代表二进制文件可在不同的平台间移植。

2.1.6 布尔代数简介

这是一套围绕着 0 和 1 建立的数学知识体系。这里记录一些基础的运算规则:

  • 非:单目运算,0 变 1,1 变 0。
  • 与:双目运算,同为 1 则得 1,反之则得 0。
  • 或:双目运算,同为 0 则得 0,反之则得 1。
  • 异或:双目运算,不同得 1,相同得 0。

2.1.7 C 语言中的位级运算

C/C++ 中有几个位运算符,对应着上述几种运算,‘|’ 是按位或,‘&’ 是按位与,‘~’ 是按位非,‘^’ 是按位异或。

2.1.8 C 语言中的逻辑运算

与或非—— ‘&&’,‘||’,‘!’。

这里和位级运算是有区别的,这些逻辑运算只会得到 true 或者 false。

2.1.9 C 语言中的移位运算

两个移位运算符:

  • <<:x << k,表示 x 向左移 k 位,并丢弃最高的 k 位,并在右侧补 k 个零。
  • >>:x >> k,分为逻辑右移和算术右移。
    • 逻辑右移,表示 x 向右移 k 位,丢弃最低的 k 位, 并在左端补 k 个零。这里一定是补 0。
    • 算术右移,表示 x 向右移 k 位,丢弃最低的 k 位, 并在左端补 k 个最高有效位。

无符号数必然是逻辑右移。对于有符号数,大部分系统都是算术右移,即用符号位的值在左端补齐。

2.2 整数表示

本章主要介绍编码整数的两种方式:

  • 有符号,可以表示正数、零、负数。
  • 无符号,只能表示正数和零。

2.2.1 整数数据类型

在 C/C++ 中,整数数据类型有多种,包括 charintlong,以及指定宽度的 int8_tint16_tint32_tint64_t

需要注意的是,long 型变量占用的字节数和机器有关,在 64 位机器上占用 8 个字节,在 32 位机器上占用 4 个字节。

另外,对于有符号的编码格式,负数的范围比正数的范围多一。

2.2.2 无符号数的编码

一个有 www 的整型数字的二进制可表示为:
x⃗=[xw−1,xw−2,...,x0] \vec{x} = [x_{w-1}, x_{w-2}, ..., x_{0}] x=[xw1,xw2,...,x0]

其对应的无符号整数的值可表示为:
B2Uw(x⃗)=.∑i=0w−1xi2i B2U_{w}(\vec{x}) \mathop{=}\limits^{.}\sum_{i=0}^{w-1}x_{i}2^i B2Uw(x)=.i=0w1xi2i

其中 B 是 binary,U 是 unsigned。

显然,www 位的无符号整数,取值范围为 [0,2w−1][0, 2^w -1][0,2w1]。在这个范围内的每一个数都有唯一一个 www 位的 x⃗\vec{x}x 与之一一对应。即函数 B2UwB2U_{w}B2Uw 是一个双射。

2.2.3 补码编码

一个有 www 的整型数字的二进制可表示为:
x⃗=[xw−1,xw−2,...,x0] \vec{x} = [x_{w-1}, x_{w-2}, ..., x_{0}] x=[xw1,xw2,...,x0]

其对应的有符号整数的值可表示为:
B2Tw(x⃗)=.−xw−12w−1+∑i=0w−2xi2i B2T_{w}(\vec{x}) \mathop{=}\limits^{.} -x_{w-1}2^{w-1} + \sum_{i=0}^{w-2}x_{i}2^i B2Tw(x)=.xw12w1+i=0w2xi2i

其中 B 是 binary,T 是 two’s complement。

在有符号数的补码编码中,对于 www 位的 x⃗\vec{x}x,我们称 xw−1x_{w-1}xw1 为符号位。当符号位为 0 时表示非负数,值域为 [0,2w−1−1][0, 2_{w-1}-1][0,2w11]。当符号位为 1 时表示负数,值域为 [−2w−1,−1][-2^{w-1},-1][2w11]

在 2.2.1 节中,提到「对于有符号的编码格式,负数的范围比正数的范围多一。」这是因为,正数和零占用了 2w−12^{w-1}2w1www 位的值编码,负数占用了 2w−12^{w-1}2w1www 位的值编码。因此,「负数的范围比正数的范围多一」,多出了一个零的位置。

函数 B2TwB2T_{w}B2Tw 也是一个双射。

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

对于大多数 C 语言实现来说,有符号数和无符号数之间的转换都是从位级的角度来看,而不是数的角度。即这种类型转换只会改变位的解读方式而不是改变位的值。

引入两个函数用来计算转换后的数值:
T2Uw(t)=t+tw−12w T2U_{w}(t) = t + t_{w-1}2^w T2Uw(t)=t+tw12w

U2Tw(u)=−uw−12w+u U2T_{w}(u) = -u_{w-1}2^w+u U2Tw(u)=uw12w+u

T2Uw(t)T2U_{w}(t)T2Uw(t) 的推导

t≥0t\ge0t0 时,显然有 T2Uw(t)=tT2U_{w}(t) = tT2Uw(t)=t

t<0t\lt 0t<0 时,t⃗\vec{t}t 对应的有符号的值记为 v0v_{0}v0,可表示为:
v0=−2w−1+∑i=0w−2ti2iv_{0}=-2^{w-1}+\sum_{i=0}^{w-2}t_{i}2^{i}v0=2w1+i=0w2ti2i

t<0t\lt 0t<0 时,t⃗\vec{t}t 对应的无符号的值记为 v1v_{1}v1,可表示为:
v1=2w−1+∑i=0w−2ti2iv_{1}=2^{w-1}+\sum_{i=0}^{w-2}t_{i}2^{i}v1=2w1+i=0w2ti2i

不难得出,当 t<0t \lt 0t<0 时,转换为无符号后,值的变化为 v1−v0=2wv_{1}-v_{0} = 2^wv1v0=2w

综上,有符号转为无符号后的数值可表示为 T2Uw(t)=t+tw−12wT2U_{w}(t) = t + t_{w-1}2^wT2Uw(t)=t+tw12w

U2T_{w}(u) 的推导

uw−1=0u_{w-1} = 0uw1=0 时,显然 U2Tw(u)=uU2T_{w}(u) = uU2Tw(u)=u

uw−1=1u_{w-1} = 1uw1=1 时,转换前的值记为 v0v_{0}v0,可表示为:
v0=2w−1+∑i=0w−2ui2i v_0 = 2^{w-1} + \sum_{i=0}^{w-2} u_{i}2^i v0=2w1+i=0w2ui2i
uw−1=1u_{w-1} = 1uw1=1 时,转换前的值记为 v1v_{1}v1,可表示为:
v1=−2w−1+∑i=0w−2ui2i v_1 = -2^{w-1} + \sum_{i=0}^{w-2} u_{i}2^i v1=2w1+i=0w2ui2i

不难得出,当 uw−1=1u_{w-1} = 1uw1=1 时,转换为有符号后,值的变化为 v1−v0=−2wv_{1}-v_{0} = -2^wv1v0=2w

综上,U2Tw(u)=−uw−12w+uU2T_{w}(u) = -u_{w-1}2^w+uU2Tw(u)=uw12w+u

小结

在进行有无符号的转换时,值的变化主要受最高位的影响。

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

在 C/C++ 中,数字常量默认是有符号的。需要用后缀 ‘u’ 或 ‘U’ 显示的指定无符号,比如 ‘0x1234U’。

对于二目运算符,如果其中一个运算数有符号而另一个无符号,则两个运算数会被隐式的转换为无符号数进行运算。这会导致一些预期之外的问题,比如下面这段代码会输出 0。

#include <stdio.h>

int main() {
  int a = -1;
  unsigned int b = 0;
  printf("res=%d\n", a < b);
  return 0;
}

2.2.6 扩展一个数字的位表示

零扩展:无符号数转换为一个更大的数据类型时,左端补零。

符号扩展:有符号数转换为一个更大的数据类型时,左端补符号位。

在 C 语言中,将一个 uint16_t 专项 int32_t 时会怎么样呢?符号转换和扩展位哪个先发生呢?答案是先扩展再转换,验证代码如下,会输出 b=ffff, c=ffffffff

#include <stdio.h>
#include <stdint.h>

int main() {
  uint16_t a = 0xFFFF;
  int32_t b = a;
  int32_t c = int16_t(a);
  printf("b=%x, c=%x\n", b, c);
  return 0;
}

2.2.7 截断数字

无论数字有无符号,都是丢弃较高的位。比如,从 www 位截断位 kkk 位,都是丢弃w−1w-1w1, w−2w-2w2, …, kkk。比较特殊的,有符号数会用 k−1k-1k1 位作为新的符号位。

用数学公式表示就是:
x′=U2Tk(xmod  2k) x^{'} = U2T_{k}(x\mod{2^k}) x=U2Tk(xmod2k)

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

注意两个细节:

  • 当二目运算符两边的整数符号属性不同时,会都隐式转换为无符号处理。
  • 无符号数做减法时,应注意差值永不为负。

2.3 整数运算

由于计算机运算的有限性,一些整数运算的结果往往不符合预期。本章将会介绍计算机运算的细微之处以帮助程序员写出更可靠的代码。

2.3.1 无符号加法

x+wuy=(x+y)mod  2wx+_w^uy = (x+y)\mod 2^wx+wuy=(x+y)mod2w

两个 www 位的无符号数,相加之和可能需要 w+1w+1w+1 位才能存下。

当说一个算术运算溢出时,是指完整的整数结果不能放到数据类型的字长限制中去。

当两个 www 位的无符号数相加发生溢出时,真实结果相当于两数相加并对 2w2^w2w 取模。

检测无符号整数加法溢出的方法: 设有三个 www 位的无符号数 sssxxxyyy,执行s=x+wuys=x+_w^uys=x+wuy 之后,若 s<xs\lt xs<x 或者 s<ys\lt ys<y 则必然溢出了。

2.3.2 补码加法

对满足 −2w−1≤x,y≤2w−1−1-2^{w-1} \le x, y \le 2^{w-1}-12w1x,y2w11 的整数 xxxyyy,有:
x+wty={x+y−2w,2w−1≤x+yx+y,−2w−1≤x+y<2w−1x+y+2w,x+y<−2w−1 x+^{t}_{w}y= \left\{ \begin{array}{c} x+y-2^w&, 2^{w-1} \le x+y\\ x+y&, -2^{w-1}\le x+y \lt 2^{w-1}\\ x+y+2^w&, x+y\lt -2^{w-1}\\ \end{array}\right. x+wty=x+y2wx+yx+y+2w,2w1x+y,2w1x+y<2w1,x+y<2w1

补码加法与无符号数加法有相同的位级表示,有如下定义:
x+wty=.U2Tw(T2Uw(x)+wuT2Uw(y)) x+^t_{w}y\mathop{=}\limits^{.}U2T_{w}(T2U_w(x) +^{u}_{w}T2U_w(y)) x+wty=.U2Tw(T2Uw(x)+wuT2Uw(y))

**检测补码加法溢出的方法:

  • x<0x \lt 0x<0y<0y \lt 0y<0 时,当且仅当 x+wty>xx+^{t}_{w}y \gt xx+wty>x 时发生溢出。
  • x>0x \gt 0x>0y>0y \gt 0y>0 时,当且仅当 x+wty<xx+^{t}_{w}y \lt xx+wty<x 时发生溢出。
  • xxxyyy 不同为正或同为负时,不可能发生溢出。

2.3.3 补码的非

对于 TMinw≤x≤TMaxwTMin_w \le x \le TMax_wTMinwxTMaxwxxx 中每个数字 xxx 都有 +wt+^t_w+wt 下的加法逆元 −wtx-^t_wxwtx,也将其称为补码的非,其值可表示为:
−wtx={TMinw,x=TMinw−x,x>TMinw -^t_wx= \left\{ \begin{array}{c} TMin_w&, x =TMin_w\\ -x&, x \gt TMin_w\\ \end{array}\right. wtx={TMinwx,x=TMinw,x>TMinw

补码的非的位级计算方法:

  1. 先对补码的每一位求非。
  2. 然后加一。

2.3.4 无符号乘法

x∗wuy=(x∗y)mod  2wx*_w^uy = (x*y)\mod 2^wxwuy=(xy)mod2w

2.3.5 补码乘法

x∗wuy=U2Tw((x∗y)mod  2w)x*_w^uy = U2T_w((x*y)\mod 2^w)xwuy=U2Tw((xy)mod2w)

检测乘法溢出的方法

  • xxxyyy 为 0 时,显然结果为 0,不存在溢出。
  • 其他情形时,当且仅当 x∗y/y==xx*y/y == xxy/y==x 时没有发生溢出。

2.3.6 乘以常数

在大多数机器上,整数乘法需要花费较多的时钟周期(比如 10 个或更多),而其他整数运算如加,减,位运算只需要一个时钟周期。因此在处理常数乘法时,编译器会尝试用若干次加法、加法以及位运算来代替乘法。

2.3.7 除以 2 的幂

三个要点:

  • 向零取整
  • 无符号整数是逻辑右移
  • 补码是算术右移

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

补码提供了一种既能表示负数又能表示正数的方式。在补码上执行加法、减法、乘法、除法都可使用与无符号算法相同的位级实现。

C 语言中的 unsigned 类型:

  • 两个 unsigned 类型的数字做算术运算的结果永不为负。
  • 当二目运算符两侧的符号类型不同时,会都隐式转换为无符号整数。

2.4 浮点数

浮点表示形如 V=x∗2yV=x*2^yV=x2y 的有理数进行编码。它对执行涉及非常大的数字(∣V∣>>0|V|>>0V>>0),非常接近于 0 的数组(∣V<<1∣|V<<1|V<<1) 的数字,以及更普遍地作为实数运算的近似值的计算,是非常有用的。

2.4.1 二进制小数

考虑形如 bmbm−1bm−2bm−3⋅⋅⋅b0.b−1b−2⋅⋅⋅b−n+1b−nb_mb_{m-1}b_{m-2}b_{m-3}···b_{0}.b_{-1}b_{-2}···b_{-n+1}b_{-n}bmbm1bm2bm3b0.b1b2bn+1bn 的数字,bib_ibi 的取值范围是 0 和 1。这种方式表示的数 b 定义如下:
b=∑i=−nm2i∗bi b = \sum_{i=-n}^{m}2^i*b_i b=i=nm2ibi

很多时候,有限位的二进制小数只能近似的表示十进制小数。比如十进制数 0.10.10.1,拆解为 12n\frac{1}{2^n}2n1 的累加和:

  • 116+x\frac{1}{16} + x161+x,此时 x=6160>132x= \frac{6}{160} \gt \frac{1}{32}x=1606>321
  • 116+132+x\frac{1}{16} + \frac{1}{32} + x161+321+x,此时 x=1160>1256x = \frac{1}{160} \gt \frac{1}{256}x=1601>2561
  • 116+132+1256+x\frac{1}{16} + \frac{1}{32} + \frac{1}{256} +x161+321+2561+x,此时 x=31280>1512x = \frac{3}{1280} \gt \frac{1}{512}x=12803>5121
  • 116+132+1256+1512+x\frac{1}{16} + \frac{1}{32} + \frac{1}{256} +\frac{1}{512} + x161+321+2561+5121+x,······

无法找到一个 xxx 使其恰好等于 12n\frac{1}{2^n}2n1,因此很多时候有限位的二进制小数只能近似的表示十进制小数。

2.4.2 IEEE 浮点表示

IEEE 浮点标准用 V=(−1)s∗M∗2EV={(-1)}^s*M*2^EV=(1)sM2E 的形式表示一个数。

  • 符号(sign)
    • 用来决定正负。
    • 一个比特。
  • 尾数(significand)
    • M 是一个二进制小数,它的范围是 [1,2−ε][1, 2-\varepsilon][1,2ε] 或者 [0,1−ε][0, 1 - \varepsilon][0,1ε],这取决于阶码的值。
    • float 中有 23 比特,double 有 52 个比特。
  • 阶码(exponent)
    • E 的作用是对浮点数进行加权,这个权重是 2E2^E2E
    • float 中有 8 比特,double 有11 比特。

编码的三种情况,根据阶码域的值进行区分:

  • 规格化的:当阶码域不全为 0 或不全为 1。
    • 此时尾数 M 的取值范围为 [1,2−ε][1, 2-\varepsilon][1,2ε],计算方式为 1+0.fn−1⋅⋅⋅f1f01+0.f_{n-1}···f_{1}f_{0}1+0.fn1f1f0
    • 此时阶码 E 的值为 BTU(ek−1⋅⋅⋅e1e0)−2k−1+1BTU(e_{k-1}···e_{1}e_{0}) - 2^{k-1}+1BTU(ek1e1e0)2k1+1,对于单精度取值范围 [−126,127][-126,127][126,127],双精度的取值范围是 [−1022,1023][-1022,1023][1022,1023]
  • 非规格化:阶码域全为 0。
    • 此时尾数 M 的取值范围为 [0,1−ε][0, 1-\varepsilon][0,1ε],计算方式为 0+0.fn−1⋅⋅⋅f1f00+0.f_{n-1}···f_{1}f_{0}0+0.fn1f1f0
    • 此时阶码 E 的值为 1−(2k−1−1)=2−2k−11-(2^{k-1}-1) = 2-2^{k-1}1(2k11)=22k1
    • 此时有 +0.0+0.0+0.0−0.0-0.00.0 之分。
  • 特殊值:阶码域全为 0。
    • 当尾数域全为 0 时,表示 +∞+∞+−∞-∞
    • 当尾数域不为 0 时,表示 NaN(Not a Number)。一些运算结果不能是实数或无穷,就会得到该值,比如 −1\sqrt{-1}1∞−∞∞-∞

2.4.3 数字示例

设阶码域为 kkk 位,尾数域为 nnn 位:

  • +0.0+0.0+0.0 总是有一个全为 0 的位表示。
  • 最小的正非规格化值的阶码域全为 0,尾数域仅最低有效位为 1。可表示为 2−n−2k−1+22^{-n-2^{k-1}+2}2n2k1+2
  • 最大的正非规格化值的阶码域全为 0,尾数域全为 1。可表示为 (1−2−n)−2k−1+2(1-2^{-n})^{-2^{k-1}+2}(12n)2k1+2
  • 最小的正规格化值的阶码域仅最低位为 1,尾数域全为 0。可表示为 20−2k−1+22^{0-2^{k-1}+2}202k1+2
  • 最大的规格化值的阶码域仅最低位为 0,尾数域全为 1。可表示为 (2−2−n)2k−1−1(2-2^{-n})^{2^{k-1}-1}(22n)2k11

本章提到一个细节,很有意思——IEEE格式使得浮点数能用使用证书排序函数来进行排序。个人觉得有点违反直觉,因此在这里证明一下。

假设有两个规格化的大于零的浮点数 a 和 b,其阶码分别为 EaE_aEaEbE_bEb,尾数分别为 MaM_aMaMbM_bMb。接下来试着证明当 Ea>EbE_a > E_bEa>Eb 时,不论 MaM_aMaMbM_bMb 取何值,都有 a>ba \gt ba>b。这里先忽略符号位、Ea=EbE_a = E_bEa=Eb、非规格化以及特殊值的情形,因为这些很符合直觉。

不妨设尾部域有 n 个比特,则尾数部分能取得的最小值为 111,最大值为 2−2−n2-2^{-n}22n。不妨让 MaM_aMa 取最小值, MbM_bMb 取最大值,则有:

  • a=Ma∗2Ea=2Ea=2Ea−Eb∗2Eba = M_a*2^{E_a}= 2^{E_a}=2^{E_a-E_b}*2^{E_b}a=Ma2Ea=2Ea=2EaEb2Eb
  • b=Mb∗2Eb=(2−2−n)∗2Ebb = M_b*2^{E_b} = (2-2^{-n})*2^{E_b}b=Mb2Eb=(22n)2Eb

因为 Ea>EbE_a>E_bEa>Eb,所以 Ea−Eb>1E_a-E_b > 1EaEb>1,所以 2Ea−Eb≥2>(2−2−n)2^{E_a-E_b} \ge 2 > (2-2^{-n})2EaEb2>(22n),所以 a>ba>ba>b

2.4.4 舍入

  • 向偶数舍入:向较接近的一方舍入,当位于中间时,向偶数舍入。
  • 向零舍入:舍入后绝对值变小或不变。
  • 向下舍入:舍入后变小或不变
  • 向上舍入:舍入后变大或不变。

向偶数舍入,或称最近舍入,是一种默认策略。这种策略最主要的优点是可以避免统计偏差。

2.4.5 浮点运算

几个属性:

  • 满足交换律。
  • 不满足结合率。因为精度问题,不同的计算顺序可能导致不同结果,比如
    • 3.14+(1e10 - 1e10) = 3.14
    • 3.14+1e10 - 1e10 = 0
  • 不满足分配率。同样是因为精度问题,比如:
    • 1e20*(1e20-1e20) = ∞
    • 1e20*1e20-1e20*1e20) = ∞ - ∞ = NaN
  • 浮点数加法慢慢组单调性属性:如果 a≥ba\ge bab,那么对于除 NaN 之外的所有值,都有 a+x≥b+xa+x\ge b+xa+xb+x

2.4.6 C语言中的浮点数

  • C 语言提供 float 和 double 两种不同的浮点数类型,分别对应单精度和双精度浮点。
  • 据我所知,应该还有 128 比特的 long double 类型。
  • 使用向偶数舍入的方式。
  • C语言不强制要求机器使用 IEEE 浮点,因此获取 −0-00+∞+∞+−∞-∞NaNNaNNaN 等特殊值,修改舍入方式等操作没有标准方法,与系统有关。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值