MEDIUM-2:计算机中的整数与浮点数
多年以后,面对食堂里的奶茶机,你是否会回想起那个有一位一看就不怎么靠谱的学长邀请你加入“格里姆”物联网校园创业团队的下午…这事还要追溯到刚开学那会儿,那时候,你对咱们的学校还不太了解,是你在网络上结识的一位学长倪寇给了你一些小小的帮助。他也许是个好人,也许是个魔人,总之他只要你帮他一个小小的忙,不需要你请他吃饭。于是,在一个平静的下午,学长邀请你加入了一个神秘组织——“格里姆”物联网校园创业团队,并随手给你发来一个文档:
【项目交接文档 - 星期四 下午 4:44】 欢迎加入“格里姆”物联网校园创业团队! 我们的主打产品——“哈吉蜜-26710型 智能全自动奶茶机”即将交付给学校食堂。
但是,上一个负责写底层固件的学长倪寇似乎在写程序的时候软脚了。现在这台机器就似乎出了那么一点点,就一点点的问题,比如说:加冰块的时候它会触发火灾警报,加珍珠的时候杯子里会出现“反物质珍珠”,而且老板用它来算账的时候发现每天都在亏钱!
作为倪寇学长为格里姆骗来…额,招纳来的全新牛马,你临危受命。你必须在天亮前,用你对整数补码和 IEEE 754 浮点数的深刻理解,修复这台机器的底层 C 语言驱动。
别抱怨了,打工人。食堂大妈明天早上六点就要用它了!
Step1:甜度与温控传感器 —— 补码与类型隐式转换
Section titled “Step1:甜度与温控传感器 —— 补码与类型隐式转换”奶茶机主控板为了省钱,用的是上个世纪的 8 位便宜单片机,所有传感器数据都塞在 8 位的寄存器里。扫了一眼程序,聪明的你立马发现,学长在处理这些数据时,好像根本没搞清“无符号”和“有符号”的区别。
【任务 1.1:到底有多甜?】
你在调试台读取到“糖分控制寄存器”的原始二进制位模式为:1001 1100。
- 如果该寄存器按无符号整数解析,这杯奶茶的甜度值是多少?
- 如果老板要求该寄存器按有符号补码整数解析(负数表示少糖),此时的甜度值是多少?
- 那么问题来了:为什么现在的计算机底层清一色都采用“补码”来存储有符号数,而不是直观的“原码”或“反码”?
【任务 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 位有符号补码计数器(范围:)在疯狂加料。结果因为超出了表示范围,杯子里的珍珠数量变成了负数,系统判定老板欠了机器几百颗珍珠。
【任务 2.1:算术溢出的惨剧】
假设当前杯子里已经有 88 颗珍珠(二进制 01011000),机器又咣当一声丢进去了 65 颗珍珠(二进制 01000001)。
- 这两个 8 位补码相加后,寄存器中实际保存的二进制位模式是多少?
- 转换为十进制后,系统认为杯子里现在有多少颗珍珠?这是正溢出还是负溢出?
- 为了防止加法溢出导致奶茶机死机,请用 C 语言补全以下检测函数。
要求:判断两个 32 位有符号 int 相加是否安全。
// 若相加不发生溢出返回 1,发生溢出返回 0int 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 竟然没有硬件乘法器!执行一次 * 号需要调用库函数,耗时几百个时钟周期,导致奶茶机出料极慢。老板要求你优化代码:需要将基础奶量 乘以 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:对学长提示的深入理解】
-
请查阅相关资料并回答:在 IEEE 754 标准中,float 与 double 分别使用多少位来表示这三部分?
-
在存储阶码时,IEEE 754 并不直接保存正负值,而是把它加上一个偏置 ——Bias后存为无符号整数。请回答:偏置值是如何计算的?这样做的意义是什么?
-
当浮点数表示的数值非常接近 0 时,规格化表示的阶码已经到最小了,这时候就会用到非规格化数。那么请查阅相关资料并回答: 非规格化数的特点是什么?
-
此外,还有两个特殊阶码模式:
- 阶码全为 1 且尾数全为 0
- 阶码全为 1 且尾数非 0
那么这两个特殊阶码模式分别指的什么?
请在markdown中回答上述出现的问题,并将正确代码也一并提交至markdown文档中。
特别注意:这部分内容仅为引入浮点数基本内容,强烈建议阅读《深入理解计算机系统》以更深入地了解浮点数
【任务 3.2:补全浮点数对照表】
由于单片机内存少得可怜,学长自己捏造了一个“8 位迷你浮点数”,完全照搬 IEEE 754 标准,具体格式如下:
符号位 :1 位(第 7 位,0 为正,1 为负)
阶码位 :4 位(第 6~3 位,偏置量 )
尾数位 :3 位(第 2~0 位)
你在一张沾满奶茶渍的草稿纸上发现了一半的解析表,请你利用 IEEE 754 知识把它补全:
| 二进制位模式 | 编码类型 (规格化 / 非规格化 / 特殊值) | 实际阶码 (十进制) | 尾数真值 (二进制小数) | 十进制最终真值 |
|---|---|---|---|---|
| 0 0110 100 | 规格化 | |||
| 0 0000 011 | (A) ____________ | (B) ______ | (C) ________ | (D) ____________ |
| 0 1111 000 | (E) ____________ | 不适用 | 不适用 | (正无穷) |
| 0 1001 010 | 规格化 | (F) ______ | (G) ________ | (H) ____________ |
【任务 3.3:阀门的极限】
- 该 8 位浮点数能表示的最小的正非规格化数的二进制位模式是什么?对应的十进制数值是多少?最大的呢?
- 阀门全开时,该 8 位浮点数能表示的最大的正规格化数的二进制位模式是什么?对应的十进制数值是多少?最小的呢?
- 比较最大正非规格化数和最小正规格化数,思考对于非规格化形式,阶码值为什么是而不是简单的?
请在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 01001100 | 01010011 |
| 01000110 01001101 | 01010010 |
| 01001000 01000111 | 01001111 |
| 01000111 01000101 | 01001110 |
【任务 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 仙人,干杯
你如是想到…
本题提交方式
Section titled “本题提交方式”出题人联系方式
Section titled “出题人联系方式”出题人:
Niko✔
QQ:2674884616
