借助 C++ 模板可以很方便的实现。
// g++ -o main main.cc -std=c++11
#include
#include
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;
}
借助 union 可以很容易实现。在下述代码中:
bool is_little_endian() {
union Layout {
uint16_t ui16;
uint8_t ui8[2];
};
Layout layout;
layout.ui16 = 0x0102;
return layout.ui8[0] == 0x02;
}
// x = 0x89ABCDEF
// y = 0x76543210
// z = 0x765432EF
z = (x&0xFF)|(y&0xFFFFFF00);
#include
#include
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;
}
X 的任何位都等于 1
!(~x)
X 的任何位都等于 0
!x
X 的最低有效字节中的位都等于 1
!uint8_t(~x)
X 的最高有效字节中的位都等于 1
!(((unsigned int)(~x)>>24))
#include
#include
int int_shifts_are_arithmetic() {
int8_t x = 0xFF;
return !(~(x>>1));
}
int main() {
printf("%d\n", int_shifts_are_arithmetic());
return 0;
}
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;
}
怎么定义奇数位呢?不妨从最低位开始编号吧,第一位,第二位,······, 依次类推。
int any_odd_one(uint32_t x) {
return !(~((x & 0x55555555) | 0xAAAAAAAA));
}
int odd_ones(unsigned x) {
int flag = 0;
while (x) {
flag += (x&1);
x >>= 1;
}
return flag & 1;
}
int leftmost_one(uint32_t x) {
uint32_t mask = 0x80000000;
while ((x & mask) == 0 && mask) {
mask >>= 1;
}
return mask;
}
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;
}
int lower_one_mask(int n) {
int mask = 0;
for (int i = 0; i < n; i++) {
(mask <<= 1) |= 0x1;
}
return mask;
}
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));
}
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;
}
错误有两处:
我的实现:
typedef unsigned packed_t;
int xbyte(packed_t word, int bytenum) {
return int8_t(word >> ((bytenum-1)<<3) & 0xFF);
}
一处错误:
当二目运算符两侧的整数符号不一致时,会都被隐式的转换为无符号整数。众所周知,两个无符号数做减法,结果恒不为负。
我的代码:
void copy_int(int val, void *buf, int maxbytes) {
if (maxbytes >= sizeof(val)) {
memcpy(buf, (void *)&val, sizeof(val));
}
}
#include
#include
#include
#include
#include
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;
}
int tsub_ok(int x, int y) {
if (y >= 0) {
// 此时只可能负溢出,若负溢出则必然大于 x
return x - y <= x;
}
// 此时只可能正溢出,若正溢出则 x - y 必然小于 x
return x - y >= x;
}
这是一种 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.当 u x ux ux 和 u y uy uy 的最高位为 0 时,传入 signed_high_prod
显然不会有任何问题。
2.当 u x ux ux 和 u y uy uy 有且有一个的最高位是 1 时,不妨设 u x ux ux 的最高位为 1,此时会有一个问题:uint64_t(x)
的高 32 位会是 0,但 int64_t(int32_t(ux))
的高 32 位是 1。
为了解决这个问题,引入 p x = u x − 2 31 px = ux - 2^{31} px=ux−231,则 u x ∗ u y ux*uy ux∗uy 可表示为:
( p x + 2 31 ) ∗ u y = p x ∗ u y + 2 31 ∗ u y (px + 2^{31})*uy=px*uy+2^{31}*uy (px+231)∗uy=px∗uy+231∗uy
p x ∗ u y px*uy px∗uy 可通过 signed_high_prod(px, uy)
求得, 2 31 ∗ u y 2^{31}*uy 231∗uy 可通过简单的位运算获得。
3.当 u x ux ux 和 u y uy uy 的最高位均为 1 时,引入 p x = u x − 2 31 px = ux-2^{31} px=ux−231 和 p y = u y − 2 31 py = uy-2^{31} py=uy−231,则 u x ∗ u y ux * uy ux∗uy 可表示为
( p x + 2 31 ) ∗ ( p y + 2 31 ) = p x ∗ p y + ( p x + p y ) ∗ 2 31 + 2 62 (px+2^{31})*(py+2^{31}) = px*py + (px+py)*2^{31} + 2^{62} (px+231)∗(py+231)=px∗py+(px+py)∗231+262
完全的代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
个人认为这道题主要在考察乘法溢出的检测方法以及错误的处理方式。
乘法溢出可用除法来检测。
我知道的错误处理有三种:
#include
#include
#include
#include
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;
}
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)
整数的除法都是向 0 取整。
常规写法可以是:
int divide_power2(int x, int k) {
return x / (1<<k);
}
但题目要求符合位级整数编码规则,只能用算术右移来代替除法,但须注意向 0 舍入的问题。
#include
#include
#include
#include
#include
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;
}
该题接受乘法溢出,因此直接用左移和加法代替乘法,并复用上一题中的代码实现除法。
int mul3div3(int x) {
return divide_power2((x<<1)+x, 2);
}
不接受溢出,可以简化为两次算术右移和一次加法:
x ∗ 3 / 4 = ( x ∗ 2 + x ) / 4 = x 2 + x 4 x*3/4 = (x*2+x)/4 = \frac{x}{2} +\frac{x}{4} x∗3/4=(x∗2+x)/4=2x+4x
先除再加绝对不会溢出了,但要考虑向零舍入的问题。因为两次除法都做了舍入,这两部分加起来可能会大于等于 1。
#include
#include
#include
#include
#include
#include
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;
}
A: 1 w − k 0 k 1^{w-k}0^k 1w−k0k
unsigned int mask = -1;
mask ^= (1<<k)-1;
B: 0 w − j − k 1 k 0 j 0^{w-j-k}1^k0^j 0w−j−k1k0j
unsigned int mask = 0;
mask ^= ((1<<(k+j))-1) ^ ((1<<j)-1);
A:(x < y) == (-x > -y)
有反例。
当 x = INT_MIN
且 y = INT_MIN+1
时,上式为 0。
B:((x+y)<<4)+y-x == 17*y+15*x
恒成立。
C:~x+~y+1==~(x+y)
恒成立。
将上式中的 ~
去掉,可得 (-x-1)+(-y-1)+1==-(x+y)+1
,显然两边恒等。
D:(ux-uy)==-(unsigned)(y-x)
恒成立。在模 2 32 2^{32} 232 下的加、减、取反的运算结果的二进制表示不会受有无符号的影响。
E:((x>>2)<<2)<=x
恒成立。左边的运算会将最低位的两个比特置零,其值必然会减小。
A:答案为 Y 2 k − 1 \frac{Y}{2^k-1} 2k−1Y,推导过程如下:
题干中所示的无穷串可表示为 Y 2 k + Y 2 2 k + Y 2 3 k + ⋅ ⋅ ⋅ ⋅ \frac{Y}{2^k} + \frac{Y}{2^{2k}} + \frac{Y}{2^{3k}} +···· 2kY+22kY+23kY+⋅⋅⋅⋅,代入等比序列求和公式可得: Y 2 k − 1 − Y 2 n k ∗ ( 2 k − 1 ) \frac{Y}{2^k-1} - \frac{Y}{2^{nk}*(2^k-1)} 2k−1Y−2nk∗(2k−1)Y,当 n n n 趋于无穷大时易得其值为 Y 2 k − 1 \frac{Y}{2^k-1} 2k−1Y。
B:代入上式可得 5 7 \frac{5}{7} 75, 6 15 \frac{6}{15} 156, 19 63 \frac{19}{63} 6319
浮点数的大小本可以直接比较二进制,但这题要求 -0
和 +0
相等,那就要特殊处理下了。
#include
#include
#include
#include
#include
#include
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);
}
b i a s = 2 k − 1 − 1 bias = 2^{k-1}-1 bias=2k−1−1
非规格化的 E E E: E = 1 − b i a s E = 1-bias E=1−bias
规格化的 E E E: E = e − b i a s E = e - bias E=e−bias
非规格化的 M M M: M = f M = f M=f
规格化的 M M M: M = 1 + f M = 1+f M=1+f
V = M ∗ 2 E V = M*2^E V=M∗2E
A. 数 7.0
指数域最高位和最低位为 1,其余为 0。
尾数域最高两位为 1,其余为 0。
B. 能够被准确描述的最大奇整数
设 m = min ( n , 2 k − 2 − b i a s ) m=\min(n, 2^{k}-2 - bias) m=min(n,2k−2−bias), 2 k − 2 − b i a s 2^{k}-2 - bias 2k−2−bias 即最大的规格化的 E E E。
尾数域最高的 m m m 位为1 ,其余为 0。
指数域的值即为 m + b i a s m + bias m+bias。
C. 最小的规格化数的倒数
先来看最小的规格化数,仅指数域最低位为 1,其余为 0,其值为 1 2 2 k − 1 − 2 \frac{1}{2^{2^{k-1}-2}} 22k−1−21。
易得倒数 2 2 k − 1 − 2 2^{2^{k-1}-2} 22k−1−2,易得 e = 2 k − 1 − 2 + b i a s = 2 k − 3 e = 2^{k-1}-2 + bias = 2^k-3 e=2k−1−2+bias=2k−3。
符号位 | 阶码位 | 整数位 | 小数位 | 十进制 | |
---|---|---|---|---|---|
最小的正非规格化数 | 0 | 0…0 | 0 | 0…1 | 2 1 − ( 2 14 − 1 ) − 63 2^{1-(2^{14}-1)-63} 21−(214−1)−63 |
最小的正规格化数 | 0 | 0…1 | 1 | 0…0 | 2 1 − ( 2 14 − 1 ) 2^{1-(2^{14}-1)} 21−(214−1) |
最大的正规格化数 | 0 | 1…0 | 1 | 1…1 | 2 2 14 − 1 ∗ ( 2 − 2 − 63 ) 2^{2^{14}-1}*(2-2^{-63}) 2214−1∗(2−2−63) |
Hex | M | E | V | D | |
---|---|---|---|---|---|
-0 | 8000 | 0 | -14 | -0 | -0.0 |
最小的大于2值 | 4001 | 1025 1024 \frac{1025}{1024} 10241025 | 1 | 1025 512 \frac{1025}{512} 5121025 | 2.001953 |
512 | 6000 | 1 | 9 | 512 | 512.0 |
最大的非规格化数 | 03FF | 1023 1024 \frac{1023}{1024} 10241023 | -14 | 1023 2 24 \frac{1023}{2^{24}} 2241023 | 0.000061 |
− ∞ -∞ −∞ | FC00 | - | - | − ∞ -∞ −∞ | -inf |
十六进制表示为 3BB0 的数 | 3BB0 | 123 64 \frac{123}{64} 64123 | -1 | 123 128 \frac{123}{128} 128123 | 0.960938 |
A 的位 | A 的值 | B 的位 | B 的值 |
---|---|---|---|
1 01110 001 | − 9 16 -\frac{9}{16} −169 | 1 0110 0010 | − 9 16 -\frac{9}{16} −169 |
0 10110 101 | 13 ∗ 2 4 13*2^4 13∗24 | 0 1110 1010 | 13 ∗ 2 4 13*2^4 13∗24 |
1 00111 110 | − 7 2 10 -\frac{7}{2^{10}} −2107 | 1 0000 0111 | − 7 2 10 -\frac{7}{2^{10}} −2107 |
0 00000 101 | 5 2 11 \frac{5}{2^{11}} 2115 | 0 0000 0001 | 5 2 10 \frac{5}{2^{10}} 2105 |
1 11011 000 | − 2 12 -2^{12} −212 | 1 1110 1111 | − 31 ∗ 2 3 -31*2^3 −31∗23 |
0 11000 100 | 3 ∗ 2 8 3*2^8 3∗28 | 0 1111 0000 | + ∞ +∞ +∞ |
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。
#include
#include
#include
#include
#include
#include
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;
}
A
0x40490FDB
对应的二进制是:
B
22 7 = Y 2 k − 1 \frac{22}{7} = \frac{Y}{2^k-1} 722=2k−1Y, Y = 22 = 14 + 7 + 1 Y = 22 = 14 + 7 + 1 Y=22=14+7+1, k = log 2 8 = 3 k = \log_{2}{8} = 3 k=log28=3
二进制小数为 11.001 001 001 …
C
针对这道题,写了一个简单的转换工具,易得在第九位开始不一样了。
#include
#include
#include
#include
#include
#include
#include
#include
// 将 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;
}
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
总结一下规则:
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
相对于人类更易使用的十进制,计算机使用二进制可以工作的更好。
本章主要介绍基于二进制的,整数和实数的编码方式、运算方式。
需要注意的是,计算机只能有有限的位对一个数字进行编码,当数字过大时其值会造成溢出。
字节,由八个比特组成,是计算机中中最小的可寻址的内存单元。内存中的每个字节都由一个唯一的数字标识,这个数字成为该字节的地址。所有可能地址的集合称为虚拟地址空间。
在十六进制表示法中,用 ‘0’ ~ ‘9’ 以及 ‘A’ ~ ‘F’ 表示十六个值,分别对应 0 到 15。
在 C/C++ 中,以 ‘0x’ 或 ‘0X’ 开头的数字常量会被当做十六进制数字处理。
不同类型的变量占用可能不同。
相同类型的变量用不同的方式编译(gcc -m32 or gcc -m64),占用字节数也可能不同。
“32 位程序” 或 “64 位程序” 指的是编译方式,而非其运行的机器类型。
大多数的 64 位机器可以运行 32 位的程序,这是一种向后兼容。显然,32 位机器可以运行 64 位的程序是不行的。
对象的地址:用对象所使用字节中最小的地址。
字节顺序:
一个容易记忆的技巧 —— 「大同小异」—— 当地址从左到右增加时,大端和人类的阅读习惯相『同』,小端则相『异』。
当两台机器通过网络通信时,需要注意字节顺序的差异,一般方法是都按约定好的网络顺序收发数据。
字符串的值不会受大小端的影响,我理解这是因为 char 是单字节的。反过来想,大小端只会影响多字节的对象。
完全相同的代码在不同平台上编译得到的二进制文件也是完全不同。因此代码能移植是指相同的代码可在不同的平台上编译并得到执行过程相同的二进制文件,但并不代表二进制文件可在不同的平台间移植。
这是一套围绕着 0 和 1 建立的数学知识体系。这里记录一些基础的运算规则:
C/C++ 中有几个位运算符,对应着上述几种运算,‘|’ 是按位或,‘&’ 是按位与,‘~’ 是按位非,‘^’ 是按位异或。
与或非—— ‘&&’,‘||’,‘!’。
这里和位级运算是有区别的,这些逻辑运算只会得到 true 或者 false。
两个移位运算符:
无符号数必然是逻辑右移。对于有符号数,大部分系统都是算术右移,即用符号位的值在左端补齐。
本章主要介绍编码整数的两种方式:
在 C/C++ 中,整数数据类型有多种,包括 char
、int
、long
,以及指定宽度的 int8_t
,int16_t
,int32_t
,int64_t
。
需要注意的是,long 型变量占用的字节数和机器有关,在 64 位机器上占用 8 个字节,在 32 位机器上占用 4 个字节。
另外,对于有符号的编码格式,负数的范围比正数的范围多一。
一个有 w w w 的整型数字的二进制可表示为:
x ⃗ = [ x w − 1 , x w − 2 , . . . , x 0 ] \vec{x} = [x_{w-1}, x_{w-2}, ..., x_{0}] x=[xw−1,xw−2,...,x0]
其对应的无符号整数的值可表示为:
B 2 U w ( x ⃗ ) = . ∑ i = 0 w − 1 x i 2 i B2U_{w}(\vec{x}) \mathop{=}\limits^{.}\sum_{i=0}^{w-1}x_{i}2^i B2Uw(x)=.i=0∑w−1xi2i
其中 B 是 binary,U 是 unsigned。
显然, w w w 位的无符号整数,取值范围为 [ 0 , 2 w − 1 ] [0, 2^w -1] [0,2w−1]。在这个范围内的每一个数都有唯一一个 w w w 位的 x ⃗ \vec{x} x 与之一一对应。即函数 B 2 U w B2U_{w} B2Uw 是一个双射。
一个有 w w w 的整型数字的二进制可表示为:
x ⃗ = [ x w − 1 , x w − 2 , . . . , x 0 ] \vec{x} = [x_{w-1}, x_{w-2}, ..., x_{0}] x=[xw−1,xw−2,...,x0]
其对应的有符号整数的值可表示为:
B 2 T w ( x ⃗ ) = . − x w − 1 2 w − 1 + ∑ i = 0 w − 2 x i 2 i B2T_{w}(\vec{x}) \mathop{=}\limits^{.} -x_{w-1}2^{w-1} + \sum_{i=0}^{w-2}x_{i}2^i B2Tw(x)=.−xw−12w−1+i=0∑w−2xi2i
其中 B 是 binary,T 是 two’s complement。
在有符号数的补码编码中,对于 w w w 位的 x ⃗ \vec{x} x,我们称 x w − 1 x_{w-1} xw−1 为符号位。当符号位为 0 时表示非负数,值域为 [ 0 , 2 w − 1 − 1 ] [0, 2_{w-1}-1] [0,2w−1−1]。当符号位为 1 时表示负数,值域为 [ − 2 w − 1 , − 1 ] [-2^{w-1},-1] [−2w−1,−1]。
在 2.2.1 节中,提到「对于有符号的编码格式,负数的范围比正数的范围多一。」这是因为,正数和零占用了 2 w − 1 2^{w-1} 2w−1 个 w w w 位的值编码,负数占用了 2 w − 1 2^{w-1} 2w−1 个 w w w 位的值编码。因此,「负数的范围比正数的范围多一」,多出了一个零的位置。
函数 B 2 T w B2T_{w} B2Tw 也是一个双射。
对于大多数 C 语言实现来说,有符号数和无符号数之间的转换都是从位级的角度来看,而不是数的角度。即这种类型转换只会改变位的解读方式而不是改变位的值。
引入两个函数用来计算转换后的数值:
T 2 U w ( t ) = t + t w − 1 2 w T2U_{w}(t) = t + t_{w-1}2^w T2Uw(t)=t+tw−12w
U 2 T w ( u ) = − u w − 1 2 w + u U2T_{w}(u) = -u_{w-1}2^w+u U2Tw(u)=−uw−12w+u
当 t ≥ 0 t\ge0 t≥0 时,显然有 T 2 U w ( t ) = t T2U_{w}(t) = t T2Uw(t)=t。
当 t < 0 t\lt 0 t<0 时, t ⃗ \vec{t} t 对应的有符号的值记为 v 0 v_{0} v0,可表示为:
v 0 = − 2 w − 1 + ∑ i = 0 w − 2 t i 2 i v_{0}=-2^{w-1}+\sum_{i=0}^{w-2}t_{i}2^{i} v0=−2w−1+i=0∑w−2ti2i
当 t < 0 t\lt 0 t<0 时, t ⃗ \vec{t} t 对应的无符号的值记为 v 1 v_{1} v1,可表示为:
v 1 = 2 w − 1 + ∑ i = 0 w − 2 t i 2 i v_{1}=2^{w-1}+\sum_{i=0}^{w-2}t_{i}2^{i} v1=2w−1+i=0∑w−2ti2i
不难得出,当 t < 0 t \lt 0 t<0 时,转换为无符号后,值的变化为 v 1 − v 0 = 2 w v_{1}-v_{0} = 2^w v1−v0=2w。
综上,有符号转为无符号后的数值可表示为 T 2 U w ( t ) = t + t w − 1 2 w T2U_{w}(t) = t + t_{w-1}2^w T2Uw(t)=t+tw−12w。
当 u w − 1 = 0 u_{w-1} = 0 uw−1=0 时,显然 U 2 T w ( u ) = u U2T_{w}(u) = u U2Tw(u)=u。
当 u w − 1 = 1 u_{w-1} = 1 uw−1=1 时,转换前的值记为 v 0 v_{0} v0,可表示为:
v 0 = 2 w − 1 + ∑ i = 0 w − 2 u i 2 i v_0 = 2^{w-1} + \sum_{i=0}^{w-2} u_{i}2^i v0=2w−1+i=0∑w−2ui2i
当 u w − 1 = 1 u_{w-1} = 1 uw−1=1 时,转换前的值记为 v 1 v_{1} v1,可表示为:
v 1 = − 2 w − 1 + ∑ i = 0 w − 2 u i 2 i v_1 = -2^{w-1} + \sum_{i=0}^{w-2} u_{i}2^i v1=−2w−1+i=0∑w−2ui2i
不难得出,当 u w − 1 = 1 u_{w-1} = 1 uw−1=1 时,转换为有符号后,值的变化为 v 1 − v 0 = − 2 w v_{1}-v_{0} = -2^w v1−v0=−2w。
综上, U 2 T w ( u ) = − u w − 1 2 w + u U2T_{w}(u) = -u_{w-1}2^w+u U2Tw(u)=−uw−12w+u。
在进行有无符号的转换时,值的变化主要受最高位的影响。
在 C/C++ 中,数字常量默认是有符号的。需要用后缀 ‘u’ 或 ‘U’ 显示的指定无符号,比如 ‘0x1234U’。
对于二目运算符,如果其中一个运算数有符号而另一个无符号,则两个运算数会被隐式的转换为无符号数进行运算。这会导致一些预期之外的问题,比如下面这段代码会输出 0。
#include
int main() {
int a = -1;
unsigned int b = 0;
printf("res=%d\n", a < b);
return 0;
}
零扩展:无符号数转换为一个更大的数据类型时,左端补零。
符号扩展:有符号数转换为一个更大的数据类型时,左端补符号位。
在 C 语言中,将一个 uint16_t 专项 int32_t 时会怎么样呢?符号转换和扩展位哪个先发生呢?答案是先扩展再转换,验证代码如下,会输出 b=ffff, c=ffffffff
。
#include
#include
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;
}
无论数字有无符号,都是丢弃较高的位。比如,从 w w w 位截断位 k k k 位,都是丢弃 w − 1 w-1 w−1, w − 2 w-2 w−2, …, k k k。比较特殊的,有符号数会用 k − 1 k-1 k−1 位作为新的符号位。
用数学公式表示就是:
x ′ = U 2 T k ( x m o d 2 k ) x^{'} = U2T_{k}(x\mod{2^k}) x′=U2Tk(xmod2k)
注意两个细节:
由于计算机运算的有限性,一些整数运算的结果往往不符合预期。本章将会介绍计算机运算的细微之处以帮助程序员写出更可靠的代码。
x + w u y = ( x + y ) m o d 2 w x+_w^uy = (x+y)\mod 2^w x+wuy=(x+y)mod2w
两个 w w w 位的无符号数,相加之和可能需要 w + 1 w+1 w+1 位才能存下。
当说一个算术运算溢出时,是指完整的整数结果不能放到数据类型的字长限制中去。
当两个 w w w 位的无符号数相加发生溢出时,真实结果相当于两数相加并对 2 w 2^w 2w 取模。
检测无符号整数加法溢出的方法: 设有三个 w w w 位的无符号数 s s s、 x x x、 y y y,执行 s = x + w u y s=x+_w^uy s=x+wuy 之后,若 s < x s\lt x s<x 或者 s < y s\lt y s<y 则必然溢出了。
对满足 − 2 w − 1 ≤ x , y ≤ 2 w − 1 − 1 -2^{w-1} \le x, y \le 2^{w-1}-1 −2w−1≤x,y≤2w−1−1 的整数 x x x 和 y y y,有:
x + w t y = { x + y − 2 w , 2 w − 1 ≤ x + y x + y , − 2 w − 1 ≤ x + y < 2 w − 1 x + y + 2 w , x + y < − 2 w − 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+y−2wx+yx+y+2w,2w−1≤x+y,−2w−1≤x+y<2w−1,x+y<−2w−1
补码加法与无符号数加法有相同的位级表示,有如下定义:
x + w t y = . U 2 T w ( T 2 U w ( x ) + w u T 2 U w ( y ) ) x+^t_{w}y\mathop{=}\limits^{.}U2T_{w}(T2U_w(x) +^{u}_{w}T2U_w(y)) x+wty=.U2Tw(T2Uw(x)+wuT2Uw(y))
**检测补码加法溢出的方法:
对于 T M i n w ≤ x ≤ T M a x w TMin_w \le x \le TMax_w TMinw≤x≤TMaxw 的 x x x 中每个数字 x x x 都有 + w t +^t_w +wt 下的加法逆元 − w t x -^t_wx −wtx,也将其称为补码的非,其值可表示为:
− w t x = { T M i n w , x = T M i n w − x , x > T M i n w -^t_wx= \left\{ \begin{array}{c} TMin_w&, x =TMin_w\\ -x&, x \gt TMin_w\\ \end{array}\right. −wtx={TMinw−x,x=TMinw,x>TMinw
补码的非的位级计算方法:
x ∗ w u y = ( x ∗ y ) m o d 2 w x*_w^uy = (x*y)\mod 2^w x∗wuy=(x∗y)mod2w
x ∗ w u y = U 2 T w ( ( x ∗ y ) m o d 2 w ) x*_w^uy = U2T_w((x*y)\mod 2^w) x∗wuy=U2Tw((x∗y)mod2w)
在大多数机器上,整数乘法需要花费较多的时钟周期(比如 10 个或更多),而其他整数运算如加,减,位运算只需要一个时钟周期。因此在处理常数乘法时,编译器会尝试用若干次加法、加法以及位运算来代替乘法。
三个要点:
补码提供了一种既能表示负数又能表示正数的方式。在补码上执行加法、减法、乘法、除法都可使用与无符号算法相同的位级实现。
C 语言中的 unsigned 类型:
浮点表示形如 V = x ∗ 2 y V=x*2^y V=x∗2y 的有理数进行编码。它对执行涉及非常大的数字( ∣ V ∣ > > 0 |V|>>0 ∣V∣>>0),非常接近于 0 的数组( ∣ V < < 1 ∣ |V<<1| ∣V<<1∣) 的数字,以及更普遍地作为实数运算的近似值的计算,是非常有用的。
考虑形如 b m b m − 1 b m − 2 b m − 3 ⋅ ⋅ ⋅ b 0 . b − 1 b − 2 ⋅ ⋅ ⋅ b − n + 1 b − n b_mb_{m-1}b_{m-2}b_{m-3}···b_{0}.b_{-1}b_{-2}···b_{-n+1}b_{-n} bmbm−1bm−2bm−3⋅⋅⋅b0.b−1b−2⋅⋅⋅b−n+1b−n 的数字, b i b_i bi 的取值范围是 0 和 1。这种方式表示的数 b 定义如下:
b = ∑ i = − n m 2 i ∗ b i b = \sum_{i=-n}^{m}2^i*b_i b=i=−n∑m2i∗bi
很多时候,有限位的二进制小数只能近似的表示十进制小数。比如十进制数 0.1 0.1 0.1,拆解为 1 2 n \frac{1}{2^n} 2n1 的累加和:
无法找到一个 x x x 使其恰好等于 1 2 n \frac{1}{2^n} 2n1,因此很多时候有限位的二进制小数只能近似的表示十进制小数。
IEEE 浮点标准用 V = ( − 1 ) s ∗ M ∗ 2 E V={(-1)}^s*M*2^E V=(−1)s∗M∗2E 的形式表示一个数。
编码的三种情况,根据阶码域的值进行区分:
设阶码域为 k k k 位,尾数域为 n n n 位:
本章提到一个细节,很有意思——IEEE格式使得浮点数能用使用证书排序函数来进行排序。个人觉得有点违反直觉,因此在这里证明一下。
假设有两个规格化的大于零的浮点数 a 和 b,其阶码分别为 E a E_a Ea 和 E b E_b Eb,尾数分别为 M a M_a Ma 和 M b M_b Mb。接下来试着证明当 E a > E b E_a > E_b Ea>Eb 时,不论 M a M_a Ma 和 M b M_b Mb 取何值,都有 a > b a \gt b a>b。这里先忽略符号位、 E a = E b E_a = E_b Ea=Eb、非规格化以及特殊值的情形,因为这些很符合直觉。
不妨设尾部域有 n 个比特,则尾数部分能取得的最小值为 1 1 1,最大值为 2 − 2 − n 2-2^{-n} 2−2−n。不妨让 M a M_a Ma 取最小值, M b M_b Mb 取最大值,则有:
因为 E a > E b E_a>E_b Ea>Eb,所以 E a − E b > 1 E_a-E_b > 1 Ea−Eb>1,所以 2 E a − E b ≥ 2 > ( 2 − 2 − n ) 2^{E_a-E_b} \ge 2 > (2-2^{-n}) 2Ea−Eb≥2>(2−2−n),所以 a > b a>b a>b。
向偶数舍入,或称最近舍入,是一种默认策略。这种策略最主要的优点是可以避免统计偏差。
几个属性: