lua浮点型精度问题的讨论

背景介绍

某天,要重新编译一个超级大工程。漫长的加载和编译时间至少要等一个上午,让我们来做一些有趣的事情吧。

前段时间,测试同学报了一个BUG,游戏比分没办法严格精确到小数点后面两位。

策划的需求是,如果数字小数点后面超出两位,直接进行数字截取前两位,不能做任何四舍五入之类的操作。例如,1.9999必须为1.99,不能变成2.00。

首先查阅框架代码,乍一看,确实也对这种情况进行了处理。 

function Format2(val)
    local tempVal = tonumber(val)
    local nTemp = tempVal * 100
    local nVal = tonumber(string.format("%.2f", nTemp / 100))
    return nVal
end

但是其实这段代码有大问题,甚至可以说基本是无效代码。

哪里出了问题呢?

请教同事查阅博客,虽然找到了一些细碎的线索,但是没有人给出一个精确地,完整的答案,或者令人信服的理论依据,那么自己来测试一下吧。

三种方案的代码分析

讨论这个问题之前,首先要明确一个问题,lua的number类型,是默认当成双精度浮点类型来运算的。也就是说number会底层当做double类型来处理,精度是16~17位 。

框架中原有的方案

Ps:对原有代码脱敏处理,保留该问题的关键逻辑,并且为了方便测试,将精度改为了1位。

function Format2(val)
    local tempVal = tonumber(val)
    local nTemp = tempVal * 10
    local nVal = tonumber(string.format("%.1f", nTemp / 10))
    return nVal
end

这里应该是模拟C++的用法,只不过用法有问题。以一位精度为例,C++中,先tempVal*10,此时结果是double类型,然后强转为int类型,就可以把小数位切掉,再除以10既可实现截取一位精度小数

但是Lua中所有的数字全都都是浮点型,没有精度转换的概念。所以这里替换成了string.format("%.1f",s),不过lua的string.format("%.1f",s)其实是有个的“四舍六入五成双”逻辑的。PS:网上大多数人说是四舍五入,是有错误的。

也就是说这一段代码根本不能满足需求,不知道为什么这个BUG隐藏了这么久。

什么是四舍六入呢

(1)被修约的数字小于5时,该数字舍去;

(2)被修约的数字大于5时,则进位;

(3)被修约的数字等于5时,要看5前面的数字,若是奇数则进位,若是偶数则将5舍掉,即修约后末尾数字都成为偶数;若5的后面还有不为“0”的任何数,则此时无论5的前面是奇数还是偶数,均应进位。

string.format("%.1f", num)的四舍六入验证

测试代码

function StringFormat(val)
    return string.format("%.1f", val)
end
-- 防盗水印:本文原创https://blog.csdn.net/lanazyit/article/details/111051387

测试数据输入输出

原值:1.5511111111111 处理后:1.6  分析 -- 原值1.555后面有值,进了一位
原值:1.55 处理后:1.6  分析  -- 1.55 -- 5后面没有值,但是5前面还有个奇数5,进了一位。
原值:1.65 处理后:1.6   分析  -- 1.65 -- 5后面没有值,但是5前面有个偶数6,舍弃。

防盗水印:本文原创https://blog.csdn.net/lanazyit/article/details/111051387


使用math.floor模拟进制转换的方案

我看到这个BUG时,第一想法:使用math.floor模拟一下double到int的强转逻辑就可以了。

function Format3(val, n)
    local n = math.pow(10, n or 1)
    val = tonumber(val)
    return math.floor(val * n) / n
end

后续验证发现,这个方法确实能用。

但是math.floor一定靠谱么?PS:最初有这个疑惑,是因为看了这个前辈的博客

math.floor一定靠谱么?

这个问题其实不难理解,不过我们还是来验证一下吧。

function FormatFloor(val)
    return math.floor(val)
end

先看一组测试数据。

输入值输出值

注意一下,从第四个数据1.99999999999999999开始,结果就出现不同程度的“错误”,比如我们输入1.99999999999999999,向下取整却得到了2。

不过这里并不能说明math.floor是异常的。因为这里其实是浮点型的机制引起的"理解偏差",浮点型在内存中其实是一个2进制的分数(C#本质论,值类型那一章有详细讲解,一般的C++书应该也会讲),我们将其转换成十进制进行输出或者其他运算操作时,2进制分数和十进制小数无法严格一一对应,所以会有一个取近似值的操作,特别是精度值溢出后,甚至会直接做四舍五入和截取的操作。

所以这里其实不是math.floor问题,而是lua直接将1.99999999999999999,直接当成了2来处理(注意第一个tostring输出时,已经将原值认为成2了)。那么1.99999999999999999在lua中被识别成了2,math.floor截取也是2,结果还是正确的。

毕竟应该不会真的有人同学过分到把double精度溢出到如此地步,再做运算吧。

专门写C++服务器的兄弟告诉我,1.999999999999999999这种值,不要考虑什么近似操作,它就是2。不管是计算机,还是我们自己阅读时,都应该将它当做2来对待,这样会省去很多麻烦。


然后我们再来考虑一个问题,我们能否不使用强行转精度的方式,来实现截取效果呢?

不使用强行转精度方式的方案

对原数字进行取余的操作,然后把余数减掉,也可以来实现这种需求。

这种操作的好处是,过程中没有任何强转,虽然每次运算都有可能出现取近似值的操作,但是完全将这些近似和转换交给底层来做,却往往比我们自己瞎操作得到的结果更理想更安全。

不过这里就仁者见仁智者见智吧。

-- 不截取任何精度截取
function Format4(val, n)
    local n = math.pow(10, n or 1)
    val = tonumber(val)
    val = val * n
    val = val - val % 1
    return val / n
end

我希望1.999999999999999999不要变成2?

如果策划再提一个变态需求,我希望1.999999999999999999不要变成2,那怎么办呢。

经过多次试验,答案是,转成字符串再进行操作。即,输入的时候就传进来字符串,不要让内存做任何精度相关的处理。然后使用字符串截取来完成截取操作。

function Format5(val, n)
    n = n or 1
    local tempVal = tostring(val)
    -- local startIndex,endIndex = string.find(tempVal, ".") -- 这里有个很严重的BUG,刚好与正则表达式冲突了
    local startIndex, endIndex = string.find(tempVal, ".", 1, true)
    if startIndex and startIndex > 0 then
        if startIndex + n < string.len(tempVal) then
            return tonumber(string.sub(tempVal, 1, startIndex + n)) -- 两位精度是3
        else
            return tonumber(string.sub(tempVal, 1, string.len(tempVal)))
        end
    else
        return tonumber(val)
    end
end

看似简单,这里遇到了一个坑。为了找到小数点的位置,我使用了string.find(tempVal, ".")来查找,但是这个参数string.find竟然默认是正则表达式识别的。传入一个“.”,相当于会识别所有的字符。导致结果一直输出错误。

-- local startIndex,endIndex = string.find(tempVal, ".") -- 这里有个很严重的BUG,刚好与正则表达式冲突了

å¨è¿éæå¥å¾çæè¿°

正确的参数填写如下,如此便可以不适用正则表达式,强行"."匹配。

   local startIndex, endIndex = string.find(tempVal, ".", 1, true)

看官老爷,文章对你有帮助嘛,点个赞可以嘛。攒攒人气,谢谢啦。

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值