Computer Systems : A Programmer's Perspective (CS:APP) 学习专栏

\(Computer \ Systems:A \ Programmer's \ Perspective \ (CSAPP)\)​​ 学习专栏

\(Chapter\ 2 \ Representing \ and \ Manipulating \ Information\)

P47二进制转补码:\(B2T_w(x)=-x_{w-1}2^{w-1}+\displaystyle\sum_{i=0}^{w-2}x_i2^i\)

P48一个非常有意思的点:补码\(Two's\ complement\)和反码\(Ones'\ complement\)\('\) 号的位置不同,来源于这样的情况

  • 对于一个非负数\(x\),我们用\(2^w - x\)(这里只有一个2)来计算\(-x\)的w位表示。而反码我们使用\([111...11]\)(w个1)来计算-x的w位反码表示

P49 Practice2.18 当我们确定位级表示后,只有最高位为\(-2^w\),例如对于32位机器来说,只有第31位为1才需要在结果里最高位表示数上加上负号,换句话说,0xff在32位机器里是正数,最高位不能表示成负数,只有十六进制表示成0xXXXXXXXX共八位时,最高位为8~f这个数才为负数。

P49 unsigned u = 4294967295u;,末尾的u表明这个数是unsigned,C的特性。

P50 \(T2U_w(x) = x+x_{w-1}2^w\)

P51 \(U2T_w(u)=-u_{w-1}2^w+u\)

P53 在C语言中,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数

P54 一个有意思的点,当扩展一个数的位表示时,无符号数采用零扩展(\(zero \ extention\)),即扩展位填0,补码数字采用符号扩展(\(sign \ extention\)),即高位填原本最高有效位的值,也就是说原本最高有效位为0,则扩充位均为0,原本最高有效位为1,则扩充位均为1.

  • 举个例子,将3位[101]扩展到4位变为[1101],我们可以发现,当扩展到w=4,最高两位的组合权重为-4与w=3时最高位的权重-4相同,这样就可以保证补码数进行位扩展时数值不会发生改变。

P56 当位数转换和符号转换同时发生时,会发生很有意思的现象,位数和符号转换的先后顺序会大大影响最终的数值,C语言标准要求先改变位大小再完成符号转换。

  • 例如将short sx转换成unsigned,先进行(int) sx完成16位→32位转换,再进行(unsigned)(int)sx完成signed→unsigned。

P57 截断数字

  • unsigned : $x' = x \mod{2^k} \(,截断为k位。\)\mod{2^k}$​能将k+1位及以上全部取模丢弃
  • two's complement : \(U2T_k(B2U_w(...)\mod{2^k})\)

P62 无符号数加法中的溢出,溢出后小于任何一个原数

  • 若溢出有\(sum = x+y-2^w\),由于x和y均为小于\(2^w\)​的无符号数,则有sum < x && sum < y

  • 无符号数求反(无符号数的加法逆元),当x!=0,我们通过让x+(-x)的值在第w+1位为1 其余位为0的情况,求出在w位下的加法逆元。

    \[-^u_wx = \begin{cases} x,\ \ \ \ \ \ \ \ \ \ x=0\\ 2^w-x, \ x>0\\ \end{cases} \]

P65 补码加法时,有一个需要注意的点就是负溢出是发生在第w+1位进为1的情况,而正溢出则是只要大于\(T_{max}\)就会发生正溢出,例如对于w=4,4+4就会正溢出,\(>T{min}=7\)

P66 计算一个位级的补码非

  • 我们之前学过了 取反+1来计算补码非
  • 我们找到x二进制表示下的从低位到高位的第一个1,将这个1左边的所有位取反就能得到x的补码非表示。
    • 举个例子,计算[1010]的补码非,第一个1出现在第2位,将其左边位取反,[0110]就是补码非。

P73 补码除法时,使用“偏置”来修饰不合适的舍入

  • 当进行负数除法时,我们采取的策略应是向零舍入,而非向下舍入,所以我们需要偏置
    • 我们利用属性\(\lceil x/y \rceil=\lfloor x+y-1/y\rfloor\)来把负数除法需要的向上取整转化为向下取整。
    • 计算数值\(x/2^k\),我们可以利用表达式(x < 0 ? x + (1 << k) - 1 : x) >> k

P74

补码除法的不同主要是正数和负数的不同,我们判断x是正或负,取决于其最高有效位,在32位字长下用x>>31就可以看需不需要用偏置来向上取整。&0xf后若为负数就可以完成+y-1的操作,在这里是\(2^4-1=15\)

int divi16
{
    return (x + (x >> 31 & 0xf)) >> 4;
}

P79 非规格化值为什么要设置偏置值位1-Bias

  • 为了与规格化值平滑连接(表示小于1的数)

P87 关于练习题2.54 x默认为int型,f默认为float型,d默认为double型

  • B. x == (int)(float)x,当x为\(T_{max}\)时等式右边的结果为\(T_{min}\),等式不成立,下面我们来分析一下

    • 我们先将\(T_{max}\)转化为IEEE浮点数表示,\(T_{max}\)就是第32位为0其余位为1,转化为IEEE浮点数表示就是Sign位为0,阶码位为127+30,尾数表示就是\(1.111..1 \times 2^{30}\),但此时float型也就是单精度IEEE,只有23位用于表示尾数,剩下位只能向偶数舍入,也就是变成Sign 0 阶码 127+30+1 小数部分0000(23个0),再转化为int,我们发现就变成了\(1\times 2^{31}\)也就是只有最高位为1,转化后就是\(T{min}\)。所以等式不成立。通过这道题我们就可以发现虽然float能表示的数可以很大,但是表示不精确,因为他的小数位只有23位,只要当前数二进制表示下最高位的1和最低位的1之间大于23位都需要进行舍入。
  • H.(f + d) - f == d 等式不成立

    • 浮点数的加法很多时候都需要舍入,举个例子\((1.0 + 1.0e20)-1.0e20\)由于舍入计算后实际上为0,我们来模拟一下

      double型二进制下小数位有64-1-11=52位,也即是说二进制表示下最高位的1和最低位的1之间大于52位都需要进行舍入。

      1.0e20也就是\(1.0\times 10^{20}\)最高位肯定大于\(2^{52}(约4.5\times10^{15})\),那么他和1.0之间的位数肯定大于52位就需要舍入,1.0就被舍入掉了,只保留结果1e20,1e20-1e20=0所以等式不成立

Homework

记录一道非常有思考价值的题目

2.65

/*Return 1 when x contains an odd number of 1s; 0 otherwise. Assume w = 32*/
int odd_ones(unsigned x);

题目的意思是说当x的二进制表达式有奇数个1时,返回1,否则返回0。这里假设int有32位。

读题都没读懂,英语还不太过关。

那么怎么来判断x里有多少个1呢?

利用逻辑运算符异或^,我们设想一下,由于异或运算1 ^ 1 = 0 ^ 0 = 0,1 ^ 0 = 0 ^ 1 = 1,也就是说当两个数进行异或运算时,当结果为0,这两个数中不可能有奇数个1,而当异或结果为0,必定两个数中有奇数个1。

\[在计算32位数x时,当其二进制表达式为a31a30...a1a0\\ ∴ Result = a31\space \otimes \space a30\space \otimes\dots\space\otimes\space a1\ \otimes\ a0\\ 我们只需要让所有位之间彼此进行异或运算,结果\&1后为0时表示有偶数个1,为1时有奇数个1。 \]

由于这么写代码太复杂,我们可以找些特性来化简,不妨将x切成两半,让x的前一部分与后一部分先进行异或运算,再将结果切成两半,再让前一部分与后一部分进行异或运算,持续切分,直到x只剩1位,这样就可以实现x的所有位之间都实现了异或运算。

遵循结合律和交换律,可以先交换将每两位之间结合,再将结果与其余部分结合。

我在代码中解释细节

/*
 * odd-ones.c
 */
#include <stdio.h>
#include <assert.h>

int odd_ones(unsigned x) { //假设x为32位
  x ^= x >> 16; //将x切分为两半 x>>16高位右移到与低位平齐,高位与低位进行异或运算
    //此时不用管x>>16后导致的高位全为0与x高位异或的结果
    //x31与x15 x30与x14 ... x15与x0之间都已异或完成,结果保留至x15~x0
  x ^= x >> 8; //再切分  现在进行x15~x8与x7~x0之间的异或运算,结果保留至x7~x0
  x ^= x >> 4; //再切分  现在进行x7~x4与x3~x0之间的异或运算,结果保留至x3~x0
  x ^= x >> 2; //再切分  现在进行x3~x2与x1~x0之间的异或运算,结果保留至x1~x0
  x ^= x >> 1; //再切分  现在进行x1与x0之间的异或运算,结果保留至x0
  x &= 0x1; //x与0x1进行&运算,取出x0的值,若为1表明有奇数个1,若为0表明有偶数个1,将结果返回
  return x;
}

int main(int argc, char* argv[]) {
  assert(odd_ones(0x10101011));
  assert(!odd_ones(0x01010101));
  return 0;
}

2.66

/*
 * Generate mask indicating leftmost 1 in x. Assume w=32
 * For example, 0xFF00 -> 0x8000, and 0x6000 -> 0x4000.
 * If x = 0, then return 0
 */
int leftmost_one(unsigned x)

leftmost_one函数是让我们在x的二进制表达式中只保留最左边一位的1,其余全部置零

这道题有和上一题类似的地方,也有截然不同的思路

模拟样例0xFF00(1111 1111 0000 0000) -> 0x8000(1000 0000 0000 0000)

那么我们怎么实现只保留最左边的1呢,不妨采用移位,让最左边的1每次右移与与右移处的位置取|,让右边所有位置都变为1,再将整个数右移一位+1,就能实现要求。

举个例子 假设最左边的1位于第b位

0110 0110 0100 0000 >>1 | x

0011 0011 0010 0000 --> 0111 0111 0110 0000 >>2此时第b位和b+1位都为1,而最左边1之前的0并没有影响

0001 1001 1001 0000 --> 0111 1111 1111 0000 >> 4 此时第b位,第b+1位,第b+2位,第b+3位均为1,所以下一步我们可以位移4位,让最左边的1之后的8位均为1

依次类推,当位移w/2位之后,最左边1之后所有位均为1,只要x存在1(用x&&1来判断),我们让x右移1位加1就可以得到答案

0111 1111 1111 1111 >> 1 + 1 = 0100 0000 0000 0000

int leftmost_one(unsigned x) {
  /*
   * first, generate a mask that all bits after leftmost one are one
   * e.g. 0xFF00 -> 0xFFFF, and 0x6000 -> 0x7FFF
   * If x = 0, get 0
   */
  x |= x >> 1; 
  x |= x >> 2;
  x |= x >> 4;
  x |= x >> 8;
  x |= x >> 16;
  /*
   * then, do (mask >> 1) + (mask && 1), in which mask && 1 deals with case x = 0, reserve leftmost bit one
   * that's we want
   */
  return (x >> 1) + (x && 1);
}

2.78、2.79、2.80是三道层层递进,紧密联系的题,放到一起看,在此不得不感叹本书编写的精妙

2.78要我们用正确的舍入方式计算\(\frac{x} {2^k}\),并且需要遵循位级整数编码规则。

C语言默认向下取整,所以当x为负数时,向下取整不满足舍入要求,我们要将其改为向上取整,需要用到偏置修正

image-20240814103027431

/*
 * divide-power2.c
 */
#include <stdio.h>
#include <assert.h>
#include <limits.h>

/*
 * Divide by power of 2, -> x/2^k
 * Assume 0 <= k < w-1
 */
int divide_power2(int x, int k) {
  int is_neg = x & INT_MIN; //&INT_MIN INT_MIN是最小的负数,只有最高位为1其余位全为0,而x是负数的必要条件就是最高位为0
  (is_neg && (x = x + (1 << k) - 1)); //若x为负数,进行偏置修正后再右移k位
  return x >> k;
}

int main(int argc, char* argv[]) {
  int x = 0x80000007;
  assert(divide_power2(x, 1) == x / 2);
  assert(divide_power2(x, 2) == x / 4);
  return 0;
}

2.79要求我们计算3x/4,需要遵循位级整数编码规则,你的代码计算3 * x也会产生溢出,要求计算3x也会溢出就要求我们先乘后除以此来保证精度,如果先除法就是对x右移,会丢失精度,这也是题目的用意。这道题也要注意进行除法时的舍入要求,正数和负数不同

/*
 * mul3div4.c
 */
#include <stdio.h>
#include <assert.h>
#include <limits.h>

/*
 * code from 2.78
 *
 * Divide by power of 2, -> x/2^k
 * Assume 0 <= k < w-1
 */
int divide_power2(int x, int k) { //和上题一样的对负数进行处理后再进行除法
  int is_neg = x & INT_MIN;
  (is_neg && (x = x + (1 << k) - 1));
  return x >> k;
}

int mul3div4(int x) {
  int mul3 = (x << 1) + x; //先进行乘法
  return divide_power2(mul3, 2);
}

int main(int argc, char* argv[]) {
  int x = 0x87654321;
  assert(mul3div4(x) == x * 3 / 4);
  return 0;
}

2.80就是将上两题积累的思路汇总,要求对于整数参数x,计算\(\frac {3*x}{4}\)的值,向零舍入

他不会溢出

本题又需要保证精度,又要保证不会溢出,怎么操作?

由于最终要除以4,我们不妨将>>2导致会丢失的低两位保存起来,单独计算,定为xx

这样x>>2就不怕低两位丢失,先除后乘也不会溢出

最后再将x和xx的计算结果相加即可

 
#include <stdio.h>
#include <limits.h>
 
int threefourths(int x)
{
	int xx = x&0x3; //利用低两位的掩码保存低两位的值,记录为xx
	int is_neg = x & INT_MIN; //判断是否是负数
	(is_neg && (xx = ((-x)&0x3))); //若x为负数,那么xx的值也需要修改,使其也表示为负数 例如 1001 利用掩码记录的xx=01,由于x是负数,-x=0111, 修改后的xx=11,这样才能计算出正确的结果
	(is_neg && (x = x + (1 << 2) - 1)); //对x进行舍入修正
	x=x>>2; //先除,此时丢失低两位
	xx=(((xx<<1)+xx)>>2);//xx只有两位先乘后除不会溢出.
	(is_neg && (xx = -xx)); //回归原值,因为最终要加到低两位上
	return (x<<1)+x+xx; //对x进行乘法,之前进行了除法此时不会溢出,我们就能计算出高两位的精确结果,加上低两位xx的计算结果就是答案
}

int main() 
{
	printf("3result=\t%d\n",threefourths(3));
	printf("-3result=\t%d\n",threefourths(-3)); //很好的例子,为什么当x为负数时,我们要修改x为-x的值
	printf("150result=\t%d\n",threefourths(150));
	printf("-150result=\t%d\n",threefourths(-150));
	printf("-10result=\t%d\n",threefourths(-10));
	return 0;
}

2.84 第一道提出了自己创新点的Homework

这道题需要补充函数的return部分,当x<=y时返回1,否则返回0。

利用符号位分成符号相同和符号不同的部分即可。

这道题有个特别需要注意的点就是在IEEE标准浮点数表示下,0.0与-0.0是不同的,-0.0的sign位是1而0.0是0,这也导致转换为Unsigned时,ux与uy差距很大,这时我们需要用一个特殊的判断条件,对于只有符号位为1时其补码表示与无符号表示的二进制表达式相等,由于做==判断有unsigned存在,所有类型会强制转换成unsigned,所以对于-0.0的特殊性,我们用如下语句判断。(ux == -ux && uy == -uy)

/*
 * float-le.c
 */
#include <stdio.h>
#include <assert.h>
#include <bits/stdc++.h>

using namespace std;

unsigned f2u(float x) {
  return *(unsigned*)&x;
}

int float_le(float x, float y) {
  unsigned ux = f2u(x);
  unsigned uy = f2u(y);

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


  return ((sx == sy) && (ux <= uy)) || (sx == 1 && sy == 0) || (ux == -ux && uy == -uy); //the last brackt is uesd for check -0.0
}

int main(int argc, char* argv[]) {
   cout << float_le(-0, +0) << endl;
  cout << (float_le(+0, -0)) << endl;
  cout << float_le(1, 1) << endl;
  cout << float_le(4, -0) << endl;
  cout << float_le(-4, 4) << endl;
  cout << (float_le(-0.0, +0.0)) << endl;
  cout << (float_le(+0.0, -0.0)) << endl;  
  return 0;
}

Datalab

logicalShift

任务描述

本关任务:补充函数logicalShift(),将x逻辑右移n位(0<=n<=31) ,将结果return返回。

操作符使用数量限制:20

不做不知道,原来到现在我对算术右移和逻辑右移的理解都是错的,我一直以为算术右移是将二进制表示中的最高的那个1右移,实际上并不是,而是将位于最高位的符号位右移,在补码表示下符号位就是最高位,例如对于32位机器,符号位就是最高位。

举个例子,将0110 0010 算术右移1位的结果是0011 0001,而将1000 0111 算术右移1位的结果是1100 0011,这下才终于搞清楚了,两种右移的区别点只在于对于符号位的处理

那么理清概念后,本题的处理就简单多了,由于题目是对32位机器进行处理,符号位位于第32位,只需要处理出1个高n位为0 低32-n位为1的数 与 算术右移后的x>>n进行&操作后,就可以算出逻辑运算后的结果了。

int logicalShift(int x, int n) 
{
	/********* Begin *********/
    int y = (1 << 31) >> n; 
    y <<= 1; //先将高n位处理成1 , 注意这里是算术右移n-1位,至于不直接写成y = (1 << 31) >> (n - 1)的原因是本题n可能为0,n-1会造成错误
    x >>= n;
    return (x & (~y));
	/********* End *********/
}

bitCount

任务描述
本关任务:补充函数bitCount(),统计x的二进制表示中1的数量,将结果return返回。

操作符使用数量限制:40

我们需要统计出x二进制表示中1的数量,不妨设想,我们让相邻两位加起来,最后得出的数就是相邻两位二进制下1的个数,例如 11 我们将高位的1也挪到低位后再相加,就是10,也就是2,满足要求,所以本题关键就是构造合适的掩码取出我们需要的位。

我们将32位先分成16组,每组有2位,计算出每组的二进制1的个数。由于每组内都要放到低位计算,所以每组的掩码就是01,先取出低位后,将32位数>>1,再与01取&后,就可以取出每组内高位的数。由于每组的掩码是01,在32位十六进制表示下就是t1=0x55555555,我们进行c = (x & t1)+(x>>1 & t1);就可以将1的个数放到每一组内。

然后再将32位分为8组,相邻4位为一组,将之前高两位和低两位1的计算结果相加放在本组内,放到低位计算,所以掩码就是0011,32位下即为t2=0x33333333,高位右移两位到低位相加,注意我们利用的是之前相邻两位计算出1的个数之后的结果,也就是 c = (c & t2) + (x >> 2 & t2)。

以此类推,再分为4组,每组8位,掩码为0x0f0f0f0f,将高4位与低4位1的个数相加,再分为2组,掩码为0x00ff00ff,将高8位与低8位1的个数相加,最后分为1组,掩码为0x0000ffff,将高16位与低16位计算结果相加,就是最终1 的个数。

int bitCount(int x) 
{
	/********* Begin *********/
	int c, t1, t2, t3, t4, t5;
    t1 = 0x55 | ((0x55) << 8);
    t1 = t1 | (t1 << 16); //t1 = 0x55555555

    t2 = 0x33 | ((0x33) << 8);
    t2 = t2 | (t2 << 16);

    t3 = 0x0f | ((0x0f) << 8);
    t3 = t3 | (t3 << 16);

    t4 = 0xff | ((0xff) << 16);
    t5 = 0xff | ((0xff) << 8); //t5 = 0x0000ffff

    c = (x & t1) + ((x >> 1) & t1);
    c = (c & t2) + ((c >> 2) & t2);
    c = (c & t3) + ((c >> 4) & t3);
    c = (c & t4) + ((c >> 8) & t4);
    c = (c & t5) + ((c >> 16) & t5);
    return c;
	/********* End *********/
}

bang

任务描述

本关任务:补充函数bang(),不使用!实现!操作符,将结果return返回。

操作符限制:~ & ^ | + << >>
操作符使用数量限制:12

本题是实现操作符!,对于!来说,运算结果只在非0数和0之间有区别,不妨列个样例找找规律,非0数1010 、0000 ,取~后 0101 、1111,我们可以发现在位数只有4位时,所有数中只有0的反码+1会溢出,我们可以利用这个特质,也就是说当我们对数x计算x | (~x + 1)后的结果中,只有当x=0时,计算结果的最高位才为0,其余情况由于不会发生溢出,x | (~x + 1)的结果中最高位必然为1。

int bang(int x) 
{
	/********* Begin *********/
    return ((x | (~x + 1)) >> 31) + 1;
	/********* End *********/	

}

fitsBits

任务描述
本关任务:补充函数fitsBits(),如果x可以只用n位补码表示则返回1,否则返回 0 (1<=n<=32)。

  • 操作符使用数量限制:15

第一次独立做出来稍微有点难度的题了,判断x是否能在n位内表示出来,实际上就是判断是否满足Tmin <= x <= Tmax的条件,注意当n=32是肯定成立的(偷懒)

int fitsBits(int x, int n) 
{
	/********* Begin *********/
	return n != 32 ? (x < (1 << (n - 1))) && (x >= -(1 << (n - 1))) : 1;
	/********* End *********/	
}

法二

一个很妙的方法

我们知道如果数x能在n位下表示,假设n为4

  • 如果x>=0,则x在第32位到第n位都应为0,例如0000....0111(7)
  • 如果x<0,则x在第32位到第n位都应为1,例如1111....1000(-8)

而当数x不能在n位下表示时,第32位到第n位就不会全为0或全为1

所以我们可以用将x >> (n - 1)的方法来判断,若x >> (n - 1)的值不等于0或-1就不能在n位下表示。

int fitsBits(int x, int n) 
{
	/********* Begin *********/
    int k;
    k = n + ~0; //~0就是-1
    x >>= k;
	return !x || !(x + 1);
	/********* End *********/	
}

isAsciiDigit

//3
/* 
 * isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
 *   Example: isAsciiDigit(0x35) = 1.
 *            isAsciiDigit(0x3a) = 0.
 *            isAsciiDigit(0x05) = 0.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 15
 *   Rating: 3
 */
int isAsciiDigit(int x) {
	int flag1 = !(x >> 4 ^ 0x30 >> 4); //check high
	int y = x & 0xf;
	int flag2 = (y + ~0xa + 1) >> 31;  //check low
	return flag1 & flag2;
}

conditional

/* 
 * conditional - same as x ? y : z 
 *   Example: conditional(2,4,5) = 4
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 16
 *   Rating: 3
 */
int conditional(int x, int y, int z) {
	int flag = !(x); //判断x是否为0
	int t = 0xff << 8 | 0xff;
	t = t << 16 | t;
	return (y & (flag + t)) + (z & (!flag + t)); //进行掩码处理,若x不为0 flag=0 输出y 就进行y & 0xffffffff操作 
}

isLessOrEqual

/* 
 * isLessOrEqual - if x <= y  then return 1, else return 0 
 *   Example: isLessOrEqual(4,5) = 1.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 24
 *   Rating: 3
 */
int isLessOrEqual(int x, int y) {
	return ((x >> 31 ^ 0) | (y >> 31 ^ 1)) & (((x + ~y + 1) >> 31) || !(x ^ y) || ((x >> 31) & !(y >> 31))); //核心就是分类判断,要注意的点就是当异号的时候做减法可能会溢出,所以将异号的情况单独判断
}

logicalNeg

/* 
 * logicalNeg - implement the ! operator, using all of 
 *              the legal operators except !
 *   Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
 *   Legal ops: ~ & ^ | + << >>
 *   Max ops: 12
 *   Rating: 4 
 */
int logicalNeg(int x) { //实现!的操作
		//return ~((((~x + 1) ^ x) >> 31) & 1) + 2; //本来我想用这种方法的,但是Tmin的补码Tmax+1==Tmin也满足(~x + 1) ^ x == 0 这个数与0的唯一区别是 0的补码和原码最高位都为0,而这个数为1 故采取|
		return ~((((~x + 1) | x) >> 31) & 1) + 2; //记住,0和其他数的区别就是 x == ~x+1(也可以理解为0的补码和0的符号位相同)同时符号位为0
}

howManyBits

/* howManyBits - return the minimum number of bits required to represent x in
 *             two's complement
 *  Examples: howManyBits(12) = 5
 *            howManyBits(298) = 10
 *            howManyBits(-5) = 4
 *            howManyBits(0)  = 1
 *            howManyBits(-1) = 1
 *            howManyBits(0x80000000) = 32
 *  Legal ops: ! ~ & ^ | + << >>
 *  Max ops: 90
 *  Rating: 4
 */
//本题是要找出能表示x的最低位数 对于正数 是最左边一位1所在位 + 1 对于负数 是最左边一位0 + 1
int howManyBits(int x) {
	int flag = x >> 31; //判断是正数还是负数
	x = ((~flag) & x) | (flag & (~x)); //将负数利用反码切换为正数,这样可以统一只需找出最高的一位1
	//先考虑高16位是否有1
	int bit_16 = (!!(x >> 16)) << 4; //'!!'用于规范化处理成一位,如果有1,!!(x >> 16)结果为1,再将结果左移四位,表示此时最高位1至少位于第16位,如果!!(x >> 16)=0那么左移运算符没影响
	//我们利用bit_16的结果对x进行移位,若高16位有1,我们就右移16位,忽略低16,继续在高16里细分,若高16没有,bit_16=0,继续找低16
	//换句话说就是利用bit_16的结果分别在高16和低16里继续找1
	x = x >> bit_16;

	//继续细分 过程同理
	int bit_8 = (!!(x >> 8)) << 3;
	x = x >> bit_8;

	int bit_4 = (!!(x >> 4)) << 2;
	x = x >> bit_4;

	int bit_2 = (!!(x >> 2)) << 1;
	x = x >> bit_2;

	int bit_1 = (!!(x >> 1));
	x = x >> bit_1;
	
	int bit_0 = x;

	return bit_16 + bit_8 + bit_4 + bit_2 + bit_1 + bit_0 + 1; //记住最低表示位数是最高位1所在位+1

}

float

/* 
 * float_twice - Return bit-level equivalent of expression 2*f for
 *   floating point argument f.
 *   Both the argument and result are passed as unsigned int's, but
 *   they are to be interpreted as the bit-level representation of
 *   single-precision floating point values.
 *   When argument is NaN, return argument
 *   Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
 *   Max ops: 30
 *   Rating: 4
 */
unsigned float_twice(unsigned uf) {
	//取出sign exp frac
	unsigned s = (uf >> 31) & 0x1;
	unsigned exp = (uf >> 23) & 0xff;
	unsigned frac = uf & 0x7fffff;
	
	//处理NaN
	if (exp == 0xff)
		return uf;
	//当处理非规格化数 即exp==0
	if (exp == 0)
	{
		frac <<= 1;
		return (s << 31) | (exp << 23) | frac;
	}

	//处理规格化数,阶码加1就能实现乘2
	exp ++ ;
	if (exp == 0xff) frac = 0; //注意这里要加上一个特判条件,当处理后阶码全为1时,要将frac清零,即表示为正无穷,不将尾数清零就会变为NaN,因为最大机器码就是0x7f7fffff,这个数乘2结果是正无穷
	return (s << 31) | (exp << 23) | frac;
	
}

float_i2f

/* 
 * float_i2f - Return bit-level equivalent of expression (float) x
 *   Result is returned as unsigned int, but
 *   it is to be interpreted as the bit-level representation of a
 *   single-precision floating point values.
 *   Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
 *   Max ops: 30
 *   Rating: 4
 */
unsigned float_i2f(int x) {
	unsigned sign, absx, flag, frac, exp;
	absx = x;
	sign = (x >> 31) & 0x1;
	if (sign) absx = -x; // 对所有负数进行正数处理0x80000000 == -0x80000000
	frac = absx;
	if (!x) return 0; //对非规格化数0特殊处理
	//用这个方法来算最高位1处于什么位置太复杂了,这题可以用if while
	// //先考虑高16位是否有1
	// int bit_16 = (!!(t >> 16)) << 4; //'!!'用于规范化处理成一位,如果有1,!!(x >> 16)结果为1,再将结果左移四位,表示此时最高位1至少位于第16位,如果!!(x >> 16)=0那么左移运算符没影响
	// //我们利用bit_16的结果对x进行移位,若高16位有1,我们就右移16位,忽略低16,继续在高16里细分,若高16没有,bit_16=0,继续找低16
	// //换句话说就是利用bit_16的结果分别在高16和低16里继续找1
	// t = t >> bit_16;

	// //继续细分 过程同理
	// int bit_8 = (!!(t >> 8)) << 3;
	// t = t >> bit_8;

	// int bit_4 = (!!(t >> 4)) << 2;
	// t = t >> bit_4;

	// int bit_2 = (!!(t >> 2)) << 1;
	// t = t >> bit_2;

	// int bit_1 = (!!(t >> 1));

	// int cnt =  bit_16 + bit_8 + bit_4 + bit_2 + bit_1;
	exp = 0;
	while (1)
	{
		if (frac & 0x80000000) break; //将最左边的1移至最高位时退出循环
		frac = frac << 1;
		exp ++ ;
	}
	frac <<= 1; //frac要再多移一位,若x的符号位原本有1,此时要先隐去这个1,frac的高位往低位数共23位即float的尾码部分
	exp ++ ;
	
	//向偶数舍入 趋向于使最低有效位为0
	if ((frac & 0x01ff) > 0x0100) flag = 1;
	else if((frac & 0x03ff) == 0x0300) flag = 1; //特殊情况 向上舍入使得最低有效位为0 例子 10.11100向上舍入到11.00 而10.10100向下舍入到10.10 偶数舍入倾向于使得最低有效位为0
	else flag = 0;
	return ((sign << 31) | ((159 - exp) << 23) | (frac >> 9)) + flag;
	//在做这道题之前我没有思考过负数在IEEE浮点数下的表示,实际上对于int里的符号位,在浮点数里符号是额外表示的,浮点数的表示形式并不是整型的缩略,也就是说在浮点数表示下0x80000000并不是换种方式实现位表示,而是要把符号位单独列出来,对所有负数先做正数处理
	//更简洁明了地说,就是IEEE浮点数下frac尾数部分的表示,是以unsigned型为标准的,所以先要将负数的补码形式转化为对应正数		
}

float_f2i

/* 
 * float_f2i - Return bit-level equivalent of expression (int) f
 *   for floating point argument f.
 *   Argument is passed as unsigned int, but
 *   it is to be interpreted as the bit-level representation of a
 *   single-precision floating point value.
 *   Anything out of range (including NaN and infinity) should return
 *   0x80000000u.
 *   Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
 *   Max ops: 30
 *   Rating: 4
 */
int float_f2i(unsigned uf) {
	int s, exp, frac, E;
	s = (uf >> 31) & 0x1;
	exp = (uf >> 23) & 0xff;
	frac = uf & 0x7fffff;
	
	//进行阶码与尾数还原
	E = exp - 127; //指数值
	frac = frac | (1 << 23); //加上小数点前的1
	
	//当E<0,uf为纯小数,整型下返回0, 显然这里也考虑了非规格化数Denormalized,这里非规格化数的E=-127,值都小于1,int下均返回0
	if (E < 0) return 0;

	//当E<23, 23位尾数无法完全保留需要左移,反之frac要右移
	if (E < 23) frac = frac >> (23 - E);
	else
	{
		frac = frac << (E - 23);
	}

	//当E>=31,int下无法表示,为infinity,返回0x80000000u;
	if (E >= 31) return 0x80000000u;
	
	//浮点数里的NaN和infinity
	if (exp == 0xff) return 0x80000000u;
	
	//负数的frac改造
	if (s)
		frac = ~frac + 1;

	return frac;	
}

纪念一下也不知道做了几十个小时的datalab

image-20240916143722650

原来很多以为早已掌握的知识,全在一个一个题目里被击破,很多细节其实都没有理解到,确实是这样的,自学的关键是一定要配合够量的题目练习,否则很多知识的细节你其实根本没有理解到位。

收获良多,补足了很多缺漏的知识(一定要学习完对应章节后马上开始lab练习,否则像这次一样战线拉得太长了就会导致很多知识已经忘记了),自己也利用阿里云整出了虚拟机,成就感满满吧,继续努力!

posted @ 2024-09-07 16:18  MsEEi  阅读(45)  评论(0)    收藏  举报