跳转到内容

MEDIUM-2:计算机中的整数与浮点数

计算机系统Banner

多年以后,面对食堂里的奶茶机,你是否会回想起那个有一位一看就不怎么靠谱的学长邀请你加入“格里姆”物联网校园创业团队的下午…这事还要追溯到刚开学那会儿,那时候,你对咱们的学校还不太了解,是你在网络上结识的一位学长倪寇给了你一些小小的帮助。他也许是个好人,也许是个魔人,总之他只要你帮他一个小小的忙,不需要你请他吃饭。于是,在一个平静的下午,学长邀请你加入了一个神秘组织——“格里姆”物联网校园创业团队,并随手给你发来一个文档:

【项目交接文档 - 星期四 下午 4:44】 欢迎加入“格里姆”物联网校园创业团队! 我们的主打产品——“哈吉蜜-26710型 智能全自动奶茶机”即将交付给学校食堂。

但是,上一个负责写底层固件的学长倪寇似乎在写程序的时候软脚了。现在这台机器就似乎出了那么一点点,就一点点的问题,比如说:加冰块的时候它会触发火灾警报,加珍珠的时候杯子里会出现“反物质珍珠”,而且老板用它来算账的时候发现每天都在亏钱!

作为倪寇学长为格里姆骗来…额,招纳来的全新牛马,你临危受命。你必须在天亮前,用你对整数补码和 IEEE 754 浮点数的深刻理解,修复这台机器的底层 C 语言驱动。

别抱怨了,打工人。食堂大妈明天早上六点就要用它了!


Step1:甜度与温控传感器 —— 补码与类型隐式转换

Section titled “Step1:甜度与温控传感器 —— 补码与类型隐式转换”

奶茶机主控板为了省钱,用的是上个世纪的 8 位便宜单片机,所有传感器数据都塞在 8 位的寄存器里。扫了一眼程序,聪明的你立马发现,学长在处理这些数据时,好像根本没搞清“无符号”和“有符号”的区别。

【任务 1.1:到底有多甜?】

你在调试台读取到“糖分控制寄存器”的原始二进制位模式为:1001 1100

  1. 如果该寄存器按无符号整数解析,这杯奶茶的甜度值是多少?
  2. 如果老板要求该寄存器按有符号补码整数解析(负数表示少糖),此时的甜度值是多少?
  3. 那么问题来了:为什么现在的计算机底层清一色都采用“补码”来存储有符号数,而不是直观的“原码”或“反码”?

【任务 1.2:冰块引发的火灾警报】

你在制冷模块的源码里发现了这样一段好像不太对劲的 C 语言逻辑:

#include <stdio.h>
void check_temperature() {
int current_temp = -10; // 当前冰水温度(摄氏度)
unsigned int warning_temp = 50; // 过热警告阈值
// 学长:只要当前温度小于 50 度,就是安全的!
if (current_temp < warning_temp) {
printf("[正常] 奶茶清凉解暑。\n");
} else {
printf("[警告!] 温度过高!触发消防喷淋系统!\n");
}
}

请问:食堂大妈想要一杯 -10 度的冰水,运行这段代码后,控制台会输出哪一行字?请结合 《深入理解计算机系统》一书中关于“有符号数与无符号数进行比较时的隐式类型转换规则”,向老板解释为什么制冷机会有这样的反应。

请在markdown中回答上述中出现的问题。


Step2:反物质珍珠 —— 整数溢出与二进制下的运算

Section titled “Step2:反物质珍珠 —— 整数溢出与二进制下的运算”

由于老板是珍珠的狂热爱好者,每次喝奶茶都要加上好几百颗珍珠当作主食来享用,机器里的 8 位有符号补码计数器(范围:[128,127][-128, 127])在疯狂加料。结果因为超出了表示范围,杯子里的珍珠数量变成了负数,系统判定老板欠了机器几百颗珍珠。

【任务 2.1:算术溢出的惨剧】

假设当前杯子里已经有 88 颗珍珠(二进制 01011000),机器又咣当一声丢进去了 65 颗珍珠(二进制 01000001)。

  1. 这两个 8 位补码相加后,寄存器中实际保存的二进制位模式是多少?
  2. 转换为十进制后,系统认为杯子里现在有多少颗珍珠?这是正溢出还是负溢出?
  3. 为了防止加法溢出导致奶茶机死机,请用 C 语言补全以下检测函数。

要求:判断两个 32 位有符号 int 相加是否安全。

// 若相加不发生溢出返回 1,发生溢出返回 0
int is_safe(int x, int y) {
int sum = x + y;
int x_sign = (x >> 31) & 1;
int y_sign = (y >> 31) & 1;
int sum_sign = (sum >> 31) & 1;
int rusult;
// 请补全代码写在TO DO与END OD TO DO之间:
//TO DO
//END OF TO DO
return result;
}

【任务 2.2:没有乘法器的悲哀】

哈?!这块廉价单片机的 CPU 竟然没有硬件乘法器!执行一次 * 号需要调用库函数,耗时几百个时钟周期,导致奶茶机出料极慢。老板要求你优化代码:需要将基础奶量 xx 乘以 29 得到最终奶量(y=x×29y = x \times 29)。请你仅使用左移位(<<)、加法(+)或减法(-),用最少的运算符写出等效的函数。

int final_milk(int x) {
int y;
// 请补全代码写在TO DO与END OD TO DO之间:
//TO DO
//END OF TO DO
return y;
}

请在markdown中回答上述出现的问题,并将正确代码也一并提交至markdown文档中。


Step3:丝滑配比阀 —— 认识IEEE 754标准下的浮点数

Section titled “Step3:丝滑配比阀 —— 认识IEEE 754标准下的浮点数”

为了调配出“极致丝滑”的奶茶,流控阀门需要用到小数。

学长贴心提示:根据 IEEE 754 浮点数标准,一个浮点数由三部分构成: 符号位(sign):1 位,决定正负。 阶码(exponent):表示数量级,类似科学计数法中的指数部分。 尾数(significand):表示有效数字部分。

【任务 3.1:对学长提示的深入理解】

  1. 请查阅相关资料并回答:在 IEEE 754 标准中,float 与 double 分别使用多少位来表示这三部分?

  2. 在存储阶码时,IEEE 754 并不直接保存正负值,而是把它加上一个偏置 ——Bias后存为无符号整数。请回答:偏置值是如何计算的?这样做的意义是什么?

  3. 当浮点数表示的数值非常接近 0 时,规格化表示的阶码已经到最小了,这时候就会用到非规格化数。那么请查阅相关资料并回答: 非规格化数的特点是什么?

  4. 此外,还有两个特殊阶码模式:

    1. 阶码全为 1 且尾数全为 0
    2. 阶码全为 1 且尾数非 0

    那么这两个特殊阶码模式分别指的什么?

请在markdown中回答上述出现的问题,并将正确代码也一并提交至markdown文档中。

特别注意:这部分内容仅为引入浮点数基本内容,强烈建议阅读《深入理解计算机系统》以更深入地了解浮点数

【任务 3.2:补全浮点数对照表】

由于单片机内存少得可怜,学长自己捏造了一个“8 位迷你浮点数”,完全照搬 IEEE 754 标准,具体格式如下:

符号位 SS:1 位(第 7 位,0 为正,1 为负)

阶码位 EE:4 位(第 6~3 位,偏置量 Bias=2411=7Bias = 2^{4-1} - 1 = 7

尾数位 MM:3 位(第 2~0 位)

你在一张沾满奶茶渍的草稿纸上发现了一半的解析表,请你利用 IEEE 754 知识把它补全:

二进制位模式编码类型 (规格化 / 非规格化 / 特殊值)实际阶码 ee (十进制)尾数真值 MM (二进制小数)十进制最终真值
0 0110 100规格化67=16 - 7 = -11.1002=1.51.100_2 = 1.51.5×21=0.751.5 \times 2^{-1} = 0.75
0 0000 011(A) ____________(B) ______(C) ________(D) ____________
0 1111 000(E) ____________不适用不适用++\infty (正无穷)
0 1001 010规格化(F) ______(G) ________(H) ____________

【任务 3.3:阀门的极限】

  1. 该 8 位浮点数能表示的最小的正非规格化数的二进制位模式是什么?对应的十进制数值是多少?最大的呢?
  2. 阀门全开时,该 8 位浮点数能表示的最大的正规格化数的二进制位模式是什么?对应的十进制数值是多少?最小的呢?
  3. 比较最大正非规格化数和最小正规格化数,思考对于非规格化形式,阶码值为什么是1Bias1 - Bias而不是简单的Bias-Bias

请在markdown中回答上述出现的问题。


Step4:消失的营业额 —— 浮点加法、舍入与结合律

Section titled “Step4:消失的营业额 —— 浮点加法、舍入与结合律”

最让老板抓狂的不是机器喷水,而是算错账。

【任务 4.1:设计加法程序】

请先阅读以下内容:

浮点数的加减运算一般由以下五个步骤完成:对阶尾数运算规格化舍入处理溢出判断

1.对阶

所谓对阶是指将两个进行运算的浮点数的阶码对齐的操作。类比平常我们用到的带阶数的加减法,对阶的目的是为使两个浮点数的尾数能够进行加减运算。在对阶的过程中有两种方式,大阶向小阶看齐,小阶向大阶看齐。但实际上我们是用小阶向大阶看齐的方式。请思考其中的原因。

2.尾数运算

尾数运算就是进行完成对阶后的尾数相加减。

3.规格化

对于IEEE754标准的浮点数来说,由于在进行上述两个定点小数的尾数相加减运算后,尾数有可能是非规格化形式,为此必须进行规格化操作。规格化操作包括左规和右规两种情况。

左规操作将尾数左移,同时阶码减值,直至尾数成为1.M的形式。例如,浮点数0.001111是非规格化的形式,需进行左规操作,将其尾数左移3位,同时阶码减3,就变成1.110000规格化形式了。

右规操作将尾数右移1位,同时阶码增1,便成为规格化的形式了。要注意的是,右规操作只需将尾数右移一位即可,这种情况出现在尾数的最高位(小数点前一位)运算时出现了进位,使尾数成为10.xxxx或11.xxxx的形式。例如,10.001100右规一位后便成为1.0001100的规格化形式了。

4.舍入处理

浮点运算在对阶或右规时,尾数需要右移,被右移出去的位会被丢掉,从而造成运算结果精度的损失。为了减少这种精度损失,可以将一定位数的移出位先保留起来,称为保护位,在规格化后用于舍入处理。IEEE754标准列出了四种可选的舍入处理方法,这里请自行查阅不详细列出。

5.溢出判断

与定点数运算不同的是,浮点数的溢出是以其运算结果的阶码的值是否产生溢出来判断的。若阶码的值超过了阶码所能表示的最大正数,则为上溢,进一步,若此时浮点数为正数,则为正上溢,记为+∞,若浮点数为负数,则为负上溢,记为-∞;若阶码的值超过了阶码所能表示的最小负数,则为下溢,进一步,若此时浮点数为正数,则为正下溢,若浮点数为负数,则为负下溢。正下溢和负下溢都作为0处理。

下面给出具体运算过程示例(以32位系统为例):

float a = 0.3; b = 1.6;

a = 0011 1110 1001 1001 1001 1001 1001 1010 Sa=0 Ea=011 1110 1 Ma=1.001 1001 1001 1001 1001 1010

b = 0011 1111 1100 1100 1100 1100 1100 1101 Sb=0 Eb=011 1111 1 Mb=1.100 1100 1100 1100 1100 1101

a + b= ?

第一步:对阶

∵ Ea < Eb Eb - Ea = 2

∴ Ma要调整为 0.0 1001 1001 1001 1001 1001 10 10

E = 011 1111 1

第二步:尾数运算

0.01001100110011001100110

+1.10011001100110011001101

1.11100110011001100110011

第三步:规格化

1.11100110011001100110011已经是个规格化数据了

第四步:舍入处理

由于在对阶时,Ma有右移,且第一次最高为1,第二次为0,所以按”0舍1入”,尾数运算结果调整为 1.11100110011001100110100

第五步:溢出判断

没有溢出,阶码不调整,所以最后的结果为

a+b = 0 01111111 11100110011001100110100 = 0011 1111 1111 0011 0011 0011 0011 0100

转为10进制

a+b = 1.90000010

要求:按照学长设计的八位迷你浮点系统,请设计一个c程序,要求为输入两个八位二进制浮点数(中间用空格隔开),然后对两个浮点数进行加法操作,只完成对阶尾数运算规格化的操作,输出规格化后的二进制表达式(不考虑溢出,若需进行舍入采取向零舍入)。为简化实现,本题只考虑非负的规格化数,忽略非规格化数和其它特殊情况

输入输出示例

输入输出
01001011 0100110001010011
01000110 0100110101010010
01001000 0100011101001111
01000111 0100010101001110

【任务 4.2:老板为什么每天都在亏钱?】

为了省事,老板坚持在算账程序里用标准的 32 位 float 类型来计算营业额。

有一天,老板查账时看到这段测试代码,直接破防了:

float boss_wallet = 1e20f; // 老板吹牛的初始资产(极大值)
float daily_expense = -1e20f; // 房租水电原料等支出(极小值)
float milk_tea_price = 15.0f; // 卖出一杯奶茶赚 15 块钱
// 算法 1:先算初始资产和支出的净值,再加奶茶钱
float revenue1 = (boss_wallet + daily_expense) + milk_tea_price; // 结果为 15.0
// 算法 2:初始资产加上(支出和奶茶钱的汇总)
float revenue2 = boss_wallet + (daily_expense + milk_tea_price); // 结果为 0.0 !!!

请你从 IEEE 754 浮点数的对阶机制、精度丢失与大数吸收的角度向老板解释:为什么同样的三个数,只是加的顺序变了,那 15 块钱就凭空消失了?(即为什么浮点数加法不满足数学上的结合律)。

请在markdown中回答上述出现的问题,并将正确代码也一并提交至markdown文档中。


【星期五 上午 5:55】 你敲下最后一个回车,编译器显示 0 errors, 0 warnings。

随着“叮”的一声脆响,奶茶机吐出了一杯温度完美、甜度适中、珍珠分量刚好的哈吉蜜奶茶。

食堂大妈推着车走了进来,你疲惫地合上笔记本,端起那杯奶茶喝了一口。

很好,没有火灾,没有反物质珍珠,老板的账本也换回了 int 型来记账。

真累啊…早知道就不该答应学长帮这所谓“一个小小的忙”,请学长吃个饭多轻松啊…

就这么想着,你忽然意识到有什么不对劲的地方。

不对啊,我什么时候答应了要请他吃饭?

为什么他就以“你不用请我吃饭了帮我个忙就好”这个理由把我搞来做这么麻烦的事情啊喂!!!

你无力地摊在椅子上,拿起哈吉蜜奶茶嗦了一口,味道还不错。

难言的喜悦从你心中涌起,虽然好像被可恶的倪寇学长坑了,但你今天又用计算机系统的知识,阻止了一场可怕的校园灾难。

你又拿起奶茶,猛嗦了一大口,回过神来,已是豪饮。

Debug 仙人,干杯

你如是想到…


提交点这里

出题人:出题人头像  Niko✔

QQ:2674884616

邮箱:2674884616@qq.com