知道为啥数据库里会有Decimal这个数据类型吗?一个是因为确实有必要(像计算金钱等必须精确处理十进制小数的场合),二个是因为如果不使用浮点数而是自己实现大数库,就不受限于基本数据类型的限制,此时计算机是可以精确处理十进制小数点的。
本质上是因为计算机不能精确计算无限的数。
但是这种无限,其实有多个层面。
第〇层:十进制是个有限小数,二进制也是个有限小数,比如 0.5, 0.25, 0.125, 0.375 等等小数。
第一层:十进制是一个有限小数,二进制是个无限小数。比如 0.2 这样的数。
第二层,十进制二进制都是无限小数,但可以表达为分数,并且分子分母都是有限的二进制数。
第三层,十进制二进制都是无限小数,不可以表达为分数,但可以通过特定的数学符号运算稳定获得。
第四层,无论在何种形态都是无限小数,无法表达为有限分数,也无法用有限的数学符号运算获得。
第四层的小数是几乎所有计算机程序都无法精确计算的。
第三层的小数,在一部分支持符号计算的数学工具内可以进行精确计算(直接以符号形态进行多项式计算)。在大部分普通应用中无法精确计算。
第二层的小数,在支持分数运算的数学工具内可以精确计算,在大部分普通应用内无法精确计算。
第一层的小数,使用一部分基于十进制运算的库可以精确计算,在大部分二进制计算的应用内无法精确计算。——原因也很明确,计算机无法精确计算无限,而这些数在二进制下是无限小数。
至于第〇层的小数,其实所有应用程序都能够精确计算,精确储存。——所有的整数其实都满足第零层规律,因此这些整数是能够精确的保存进一个浮点数的,当浮点数运算的前后结果都在第〇层以内时,这样的小数也是可以做到精确计算的。
所以,这个问题其实描述不准确,准确的说法应该是满足第〇层规律的小数可以直接使用CPU硬件指令直接进行精确计算,满足1,2,3,层规律的小数无法直接使用CPU指令精确计算,只能使用特殊的软件算法实现精确计算,但由于效率不高,它们仅仅用于数学工具类应用,常规应用并不使用类似的方法。
既然这个问题没有附背景,那就直接按照最根本的原理来回答吧:
因为实数不可数,以及可计算数是否相等不可判定
常见的计算机内的数值表达方式实际上有很多种,如整数、定点数、浮点数大部分人都很熟悉,但实际上不仅仅有二进制,还有十进制数,比如Python的decimal库,可以试试看:
>>> from decimal import Decimal >>> 1.2 - 1.0 0.19999999999999996 >>> Decimal('1.2') - Decimal('1.0') Decimal('0.2')
可以通用地将计算机内数值的表达方式分成两种:一种占用的字节数固定或者有上限;另一种使用的字节数可以任意多,当然具体表示某个数值的时候仍然是有限多个字节,只是不管多少个字节都一定有存不下的数。不管哪种表达方式,使用N个字节,也就是8N个二进制位的时候,能表达的数值的总数一定不超过 种,也就是说能表示的数值总数是有限的,不同的存储格式只是将它们对应到不同的数值上而已,例如整数、定点数就让这些数值在表达范围内均匀分布,而浮点数则为了拓宽表达范围,让数值在绝对值较大的时候间距增大,总的可表示数的数量仍然不超过上限(因为规范甚至有一些是对应到非正规数如inf/-inf/NaN而非真正的实数)。有限多个数当然不能任意精度地表示任意实数。
另一种可以使用任意多个字节,这样可表达范围就比上一情况多了,可以表示无限多的数值,但仅限于可数无穷,因此唯一表示任意实数显然是不可行的。例如Python里的int是高精度数,可以表示任意位的整数(当然前提是你有足够大的内存);Python还带有一个有理数库fractions,在有理数范围内,它真的可以达到完全精确:
>>> Decimal('1') / Decimal('199795') * Decimal('199794') + Decimal('1') / Decimal('199795') * Decimal('1') Decimal('0.9999999999999999999999999999') >>> Fraction('1') / Fraction('199795') * Fraction('199794') + Fraction('1') / Fraction('199795') * Fraction('1') Fraction(1, 1)
然而如果开个平方根,它就还得退回到不精确的浮点数上,因为非完全平方数的平方根不是有理数,不能用分数的形式来表示。
能不能设计一个库连开平方根这样的操作也保证精确呢?实际上也是可行的,我们可以用一个整系数多项式加一个序号来表示这个整系数多项式的某个根(顺序按照某种规则规定),这样可以精确表示全体代数数,虽然没有人设计开发过这样的库,但是原理上是可行的(大概吧……)。
即便如此,如果引入exp/log/三角函数之类的函数,还是不可避免会遇到超越数,这时候就没有那么好办了,虽然不难证明,不管引入多少函数,只要运算有限次,能得到的数仍然是可数多,这样单纯存储表示某个数仍然是可能的,比如直接将整个运算过程用表达式树存起来,但我们没有能力求出它们的某个唯一表示形式,也没有办法精确地比较两个数是否相等,这就不能算是运算了,因为拿不到精确的结果。至少目前为止,人类还并不知道一种能够完全精确地判断两个包含exp/log/三角函数的表达式是否得到精确相等的结果,或者精确判断两个数值谁更大谁更小的方法,因此至少目前为止,如果你的运算要包含exp/log/三角函数,那么精确计算的的确确是不可能的。
实际上,更多的计算任务会比这些函数更加复杂,比如说需要计算无穷级数或者定积分,这些问题通常来说都在某种程度上进行了求极限的操作,例如无穷级数就是关于累加次数的极限,定积分就是关于细分程度的极限;还有一些任务的已知的计算方法只能通过极限进行,其实也包括前面的exp/log这样的函数,它们的计算方式通常都和无穷级数或者牛顿迭代之类的方法有一定联系。从更高的层次上来看,我们可以认为这些数值每个都可以对应到一个图灵机程序,这个程序可以根据需要将这个数值计算到任意高的精度上(图灵机是抽象机器,可以认为存储是无限大的),但是确定性地判定任意两个图灵机程序是否等价本身是不可计算的,意味着只要采用图灵机的架构,那么这样的数值是否相等就一定是不可判定的,这就从理论上最终否定了所有“可计算数”的精确计算(哪怕只是比较是否相等)的可能性。
总结来说的话,首先实数本身不可数,因而不可能直接表达任意实数;其次,虽然可计算的数没有全体实数那么多,但一旦需要进行的运算复杂到某种程度,这时无论技术如何进步都是不可能在计算机里对这样的数进行精确表示、运算和判定是否相等的,因而有限精度是必须的。从实用的角度上来说,如果你的计算范围仅限于整数或者有理数,那么完全精确计算是可行的,虽然通常要付出更大的代价;如果涉及到更复杂的运算,那么采用不精确的计算是不可避免的,不过总是可以通过精妙的程序设计让误差控制在需要的范围以下,这在实用的角度上已经足够了。通常来说,需要的精度越高,计算的代价会成倍甚至成指数增加,因此从成本上来说,选择适合自己的精度也是必须的。