概述
57
2020-01-12 14:23:32 +08:00 2
"浮点运算结果不准确"恰恰是预期行为,因为浮点数的定义就决定了它必然”不准确“,浮点数本身就是一个 approximation。
这和 primitive 的整数有有符号和无符号之分、范围限制、溢出问题之类某种程度上是同一个性质。同样我们可以问:primitive 整数有溢出是不是 bug ?
不同的在于,primitive 整数溢出可以是 bug——一些编程语言包含了边界检查的语义,在出现溢出时会抛出异常。但是浮点运算从定义上就是不准确的,因此不算做 bug。
之所以说它们有共同之处,是因为它们都是在计算机上实现计算过程时做出的妥协。
比如说,Java 有 Primitive Type 和非 Primitive Type 之分,Primitive Type 成为了 Java 的”一切皆对象“原则上的一个漏洞( web.archive.org/web/20081012113943/http://www.eecs.harvard.edu/~greg/cs256sp2005/lec15.txt )。可见 Primitive Type 造成的问题不仅仅是“运算不精确”一个。楼主的问题”为什么至今仍然有很多开发语言有这个问题呢“首先需要解决的是“既然 Primitive Type 如此麻烦,为什么还要保留 Primitive Type 呢?”并且不只 Java,几乎所有编程语言都具有 Primitive Type,这是为什么呢?
理论上,使用非常精简的规则,即可表达出所有计算需要的东西,包括数字( https://en.wikipedia.org/wiki/Church_encoding ),因此,Primitive Type 在理论上是不必要的。但是在实际应用的编程语言中,很少有大量应用 Church Encoding 的,相反,Primitive Type 被广泛使用。
因为“In theory, theory and practice are the same. In practice, they are not.”
如果把整个计算机看做一个系统的话,编程语言是开发者和计算机之间的接口,ISA 是软件和硬件之间的接口,这些接口都是越简洁越好。图灵机的基本规则也非常简单,如果只是实现一个计算机的话,几条指令完全够用了。
上世纪著名的 PDP 系列有很多是硬件不支持乘法 /除法操作的。Intel 在 8086 之前也不支持,而一直到 486 之前,浮点运算还要靠 Coprocessor。一直到现在的 RISCV,乘除法、浮点还是 Extension,核心指令集只有不到 50 条指令。这个规模并不比 UNIX 最早用的早期 PDP-11 要小多少。
同样的原则可以推出,GPU 所做的工作 CPU 也可以做,GPU 是完全没必要的东西。老黄赚的钱全是炒概念的智商税,AMD 也没必要养着 RTG。包括挖矿什么的也可以通通使用 CPU 搞定。
但是另一方面,RISC-V 也有各种扩展(很多还没做完),现在的 x86 算上各种扩展已经有了一千多条指令。ARM 的核心指令集规模和其他 RISC 类似,但是随便一个 SIMD 扩展甩出来就是几十上百条指令。而现在不仅 CPU 和 GPU 很火,还加入了乱七八糟的 FPGA、TPU 之类的东西,老黄还多此一举的把 RT core 做进了 GPU 里面。这似乎与我们所追求的简洁构成了某种矛盾,最重要的是,老黄又赚了我们一波智商税。
我在 V 站说明过“编程语言的设计可以影响给到编译器的程序信息的量,进而影响优化编译器的优化效果”( https://v2ex.com/t/632869#r_8401400 )以及“高级语言抽象好,低级语言上限高”( https://v2ex.com/t/594287#r_7803885 )的原理。同样的原则也适用在硬件上——硬件在执行计算时需要“我要执行什么计算”的信息,而 Church Encoding 之类的通用表示方法之所以没法用,就是因为它太通用了导致硬件得到的信息太少,执行效率太低——一个 C++ 程序可以被编译为机器码,但是给你一坨 C++ 编译出来的机器码(经过较多优化,无调试符号),不能反编译出原始的 C++ 程序,甚至就算再把原始 C++ 程序给你,把 C++ 代码和机器代码的位置对应起来在没有调试符号的情况下都是个难题,大量的高层程序信息在转换为具体的、底层的机器表示的过程中逐渐不可逆地丢失了。现代 CPU 会利用各种手段以利用更多的程序信息,达到更高的执行效率,但是当程序信息本身就不足时,硬件厂商也无能为力,所以现在硬件厂商宁愿教育开发者多写 “Modern Code” (虽然最后开发者还是更喜欢 Electron )来最大化硬件使用率,提高执行效率(这里的极端便是上个十年的 VLIW 架构——抛弃 CPU 部分的 hack 来简化硬件,寄希望于软件(包括编译器)能给出更多的信息)。另一方面,硬件厂商需要给出用来表示高效代码执行所需的接口,这就是各种乱七八糟的指令集扩展和非通用硬件。
硬件本身则通过 Chip Specialization 的方法,来最大化这些信息的利用。什么是 Specialization ?比如说我们知道整数 a * 8 等价于 a << 3,那么编译器如果有“a 是整数”和“表达式 a * 8”这样的信息,便可以把 a * 8 specialize 为 a << 3。Specialization 要求获得足够的信息,如果编译器不知道 a 的类型,或者遇到“a * b”这样的表达式( b 的值无法推导),就没有办法做 Specialization。
半导体中的 Chip Specialization 则是指对特定已知的计算,直接使用芯片硬件电路实现,而不是用通用的方法(先实现一个图灵完全的指令集,弄一个 CPU,再写软件实现算法)。这样做可以用更少的功耗,对特定计算实现更高的性能——因为算法直接在硬件实现,并且会用经过优化的方法实现。用软件实现和用优化的硬件实现的区别,就像用 Python 实现 FFT 算法性能不如直接调用 scipy 库一样——Python 直接实现的算法,在运行时除了你自己,计算机是不知道它在做 FFT 的,这个信息在源码之后就被丢失了。scipy 库则可以利用“我现在正在做的是 FFT”这项信息给出最优的实现,前提是你通过“调库”的方式,把这个信息告诉计算机。
GPU 是对图形运算的 Specialization,GPGPU 则是对 SIMT 模型的 Specialization,RT core 是对光线追踪算法的 Specialization,现在手机厂商争相加入的 AI 芯片,则是对 AI 算法的 Specialization,苹果为新 Mac Pro 推出了 Afterburner 加速卡,貌似是用 FPGA 做的,可以看做是对 ProRes 格式的 Specialization。
当然,越是做 Chip Specialization,就越会发现 Chip Specialization 的能力是有极限的,这就是现在半导体所讲的 The Accelerator Wall ( https://parallel.princeton.edu/papers/wall-hpca19.pdf )——芯片厂商在把常见算法都用硬件实现一遍之后就又没事可做了,现在看上去大家都在搞 Chip Specialization,只是因为之前都在搞通用处理器,没有来得及充分利用 Chip Specialization 的潜力而已,等到这波“红利”吃完了,还是会回到通过爬制程工艺,堆核扩大芯片规模来提升性能和能耗比的老路。
Chip Specialization 不仅体现在 AI、挖矿、光追等“高大上”领域,不同位数的整数运算、乘除法运算、浮点运算同样也属于 Chip Specialization,只不过这些早就普及了。也正是因为这些东西普及率高,工业上的通用编程语言才会设计 Primitive Type,作用正是允许程序员将优化需要的信息 encode 在程序中,从而方便编译器或硬件的 Specialization (一般做成 Primitive Type 的,在整个系统栈中的某个或多个位置都会有 Specialization,比如上面提到某些处理器没有提供硬件乘法指令,这时编译器会调用一个优化过的库函数来做乘法)。
需要注意的是,Primitive Type 和底层 Specialization 的对应关系,并不能动摇 Primitive Type 本身更像个 hack 的性质。Primitive Type 实际在程序语言中形成了某种边界模糊的 DSL,而将 Specialization 抽象为 DSL 的做法在最近越来越 explicit,比如 CUDA 则是程序员为 NVIDIA GPGPU 这一 Specialization 提供计算信息的工具,同样的现象出现在 AI 领域。
所以 C 语言标准里会针对各种 Primitive Type 做出“至少 32 位”之类奇怪的限制,因为这些 Primitive Type 直接对应硬件或软件的 Specialization 或某个可以用来做 Specialization 的标准。
无限范围 /精度的整数和实数在理论上是不能使用有限空间存储的,并且实现会比固定范围更复杂,而大多数情况下,其带来的好处无法 justify 其成本。最后形成的妥协便是:使用固定位数、有限精度的整数和浮点数来进行大多数的计算。在编程语言中做 Primitive Type,在编译器和库中针对这些类型做优化,在硬件中针对这些类型的运算做 Specialization。
“任何人都不想得到不准确的结果吧”同样的话可以这么说“任何人都不想内存空间受限制吧”“任何人都不想网速有个最大值吧”“任何人都不想一次航班要好几个小时吧”“任何人都不想钱能花完吧”。
浮点数只是系统给你提供的一个选择,当固定位数的整数 /浮点数无法满足你的需求时,你可以选择使用其他手段,就像在编程语言中定义新的函数、类型一样。比如使用符号计算,把你的公式本身(而不是公式运算出的值)存储起来,计算机来做化简,什么数都可以表示。如果楼主够厉害,够有钱,可以使用 Chip Specialization 的方式把这套系统用硬件实现,并做成编程语言的 Primitive Type (或一套 DSL )。就不会有这种问题了。
真正的 bug 出在楼主的认识里。“浮点数”从定义上就是有理数的一个子集而不是实数,也不是有理数。各种 Primitive Integer Type 一般也对应的是整数的一个子集而不是整数。楼主将“浮点数”默认为“小数”或“实数”导致出现了这样的疑问。但是有没有想过,如果“浮点数”等于“实数”的话,为什么要叫“浮点数”这个奇怪的名字而不是“实数”呢。
当然有些编程语言不负责任地定义了一个名字叫 “real” 类型,却用浮点数实现。real 这个名字上包含所有实数,但是只能包含有理数的一个子集。同理有些语言有名叫“int”或“integer”的类型,但是只能包含整数的一个子集。这种挂羊头卖狗肉的行为已经超越了 bug 的范畴,我个人是支持批判的。但是如果名为 “float”“single”“double”,用浮点数实现,只能表示部分有理数,这是预期行为,不是 bug。
最后
以上就是无私丝袜为你收集整理的python123贴吧_浮点运算结果不准确算不算是 bug?任何人都不想得到不准确的结果吧...的全部内容,希望文章能够帮你解决python123贴吧_浮点运算结果不准确算不算是 bug?任何人都不想得到不准确的结果吧...所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复