MIPS汇编中double类型精度维护及小数位数修正方法
MIPS汇编浮点数读取与精度问题解决
我用MARS编写MIPS汇编程序,从txt文件读取数字存入向量,部分数字是double类型。程序逻辑整体能跑,但部分数字计算出错,比如文件里的0.672存成了0.6719999999999999,加法操作偶尔也错。试过切换单精度,问题依旧,求解决办法。
代码(葡萄牙语注释已翻译为中文)
.data fim: .double -1.0 zero: .double 0.0 dez: .double 10.0 filename: .asciiz "xtrain.txt" quebra_linha: .asciiz "\n" espaco: .asciiz " " buffer: .space 17408 .align 3 vetor: .space 4000 .text ler_arq: # 读取文件 la $a0, filename li $a1, 0 li $a2, 0 li $v0, 13 syscall load: # 将文件加载到缓冲区 move $a0, $v0 la $a1, buffer li $a2, 17408 li $v0, 14 syscall ler_nums: # 逐字节读取文件 la $s0, vetor move $s1, $s0 la $t0, buffer l.d $f10, dez copy_num: lb $t1, 0($t0) # 将当前字符加载到t1 bne $t1, '.', cont # 如果不是小数点,跳转到后续步骤 li $t3, 1 # 如果是小数点,将t3设为1(表示进入小数部分) addiu $t0, $t0, 1 lb $t1, 0($t0) # 移动到下一字节 cont: beq $t1, ',', num_end beq $t1, '\r', num_end beq $t1, '\n', fim_copia # 如果是逗号或换行,结束当前数字读取 beq $t1, 0, end # 如果文件结束,跳转到代码末尾 subu $t1, $t1, 48 # 将ASCII转换为整数 beq $t3, 1, le_double # 如果t3为1,跳转到小数读取逻辑 le_int: mul $t2, $t2, 10 # 将之前存储的整数乘以10 add $t2, $t2, $t1 # 加上当前字符对应的数字 j fim_copia le_double: mtc1 $t1, $f0 cvt.d.w $f0, $f0 # 转换为double类型 addiu $t4, $t4, 1 # 10的幂次计数器 # 循环将数字除以10^t4 loop_double: div.d $f0, $f0, $f10 addiu $t5, $t5, 1 blt $t5, $t4, loop_double add.d $f2, $f2, $f0 li $t5, 0 j fim_copia num_end: mtc1 $t2, $f4 cvt.d.w $f4, $f4 # 转换为double类型 add.d $f4, $f4, $f2 s.d $f4, 0($s1) li $t2, 0 # 重置整数累加器 li $t3, 0 # 将t3设为0(退出小数模式) li $t4, 0 # 重置10的幂次计数器 l.d $f0, zero # 重置f0 l.d $f2, zero addiu $s1, $s1, 8 # 移动向量中的位置 j fim_copia fim_copia: addiu $t0, $t0, 1 j copy_num end: mtc1 $t2, $f0 cvt.d.w $f0, $f0 s.d $f0, 0($s1) # 将最后一个数字存入向量 addiu $s1, $s1, 8 l.d $f0, fim # 将-1.0存入向量末尾作为结束标记 s.d $f0, 0($s1) l.d $f12, 0($s0) # 将向量起始位置传入f12作为参数 # 循环打印程序中的每一行和数字 loop: addiu $t9, $t9, 1 li $v0, 3 syscall la $a0, espaco li $v0, 4 syscall addi $s0, $s0, 8 l.d $f12, 0($s0) bne $t9, 8, cont_fq la $a0, quebra_linha li $v0, 4 syscall li $t9, 0 cont_fq: c.eq.d $f12, $f0 bc1f loop
解决办法
1. 接受浮点数的固有精度限制
像0.672这类十进制小数,无法用二进制浮点数(单/双精度)精确表示——二进制只能精准存储分母为2的幂的分数,其他小数会被近似截断,显示时就会出现类似0.6719999999999999的结果。这是正常现象,不是代码bug。
- 如果必须精确表示,需要自行实现十进制浮点运算逻辑(MARS无原生支持);
- 仅需显示正确的话,可自定义输出格式化,限制小数位数(比如保留3位)。
2. 修复小数累加的逻辑错误
当前代码中,小数位处理用循环多次除以10来计算权重,会累积额外误差。改用权重递减法更高效且误差更小:
- 初始化小数权重为0.1,每处理一位小数就将权重除以10;
- 当前数字乘以权重后直接加到小数累加器,避免多次除法操作。
修改后的le_double段示例:
le_double: mtc1 $t1, $f0 cvt.d.w $f0, $f0 # 转换为double # 初始化或更新权重 beq $t4, 0, init_weight div.d $f6, $f6, $f10 # 权重 = 权重 / 10 j calc_decimal init_weight: li $t6, 1 mtc1 $t6, $f6 cvt.d.w $f6, $f6 div.d $f6, $f6, $f10 # 初始权重设为0.1 calc_decimal: mul.d $f0, $f0, $f6 # 当前数字 × 权重 add.d $f2, $f2, $f0 addiu $t4, $t4, 1 # 标记已处理小数位 j fim_copia
3. 初始化未清零的寄存器
当前代码中,ler_nums段未初始化$t2、$f2等累加变量,第一次运行时这些变量的值是随机的,会导致第一个数字计算错误,也是加法出错的核心原因之一。在ler_nums开头添加初始化代码:
ler_nums: # 逐字节读取文件 la $s0, vetor move $s1, $s0 la $t0, buffer l.d $f10, dez l.d $f2, zero # 初始化小数累加器 li $t2, 0 # 初始化整数累加器 li $t3, 0 # 初始化小数标记 li $t4, 0 # 初始化小数位计数器
4. 优化输出格式化
MARS的syscall 3会输出浮点数的所有有效位,放大近似误差。可以自己实现格式化函数,指定保留的小数位数:
- 将浮点数乘以10^n(n为要保留的位数),转换为整数后四舍五入;
- 拆分整数和小数部分分别打印,比如0.6719999999999999处理后会显示为0.672。
内容的提问来源于stack exchange,提问作者despinxz
相关产品推荐
相关产品推荐

