为啥整数除法在许多脚本语言中舍入?
Posted
技术标签:
【中文标题】为啥整数除法在许多脚本语言中舍入?【英文标题】:Why does integer division round down in many scripting languages?为什么整数除法在许多脚本语言中舍入? 【发布时间】:2018-05-20 09:37:14 【问题描述】:在我测试过的语言中,- (x div y )
不等于-x div y
;我在 Python 中测试了 //
,在 Ruby 中测试了 /
,在 Perl 6 中测试了 div
; C has a similar behavior.
这种行为通常符合规范,因为div
通常被定义为the rounding down of the result of the division,但是从算术的角度来看它没有多大意义,因为它使div
以不同的方式表现取决于标志,它会引起混淆,例如this post on how it is done in Python。
这个设计决定背后是否有一些具体的理由,或者只是 div
是从头开始定义的?显然 Guido van Rossum uses a coherency argument 在一篇博文中解释了它是如何在 Python 中完成的,但如果你选择四舍五入,你也可以保持一致性。
(灵感来自this question by PMurias in the #perl6 IRC channel)
【问题讨论】:
FWIW,在 Python 中//
称为楼层划分。试试这个:.7 // .1
。请注意,它不会评估为 int
。
FWIW,在 Python 中,您可以使用双重否定来获得上限除法:例如 -(-23 // 10)
【参考方案1】:
理想情况下,我们希望有两个操作div
和mod
,满足每个b>0
:
(a div b) * b + (a mod b) = a
0 <= (a mod b) < b
(-a) div b = -(a div b)
然而,这在数学上是不可能的。如果以上都是真的,我们就会有
1 div 2 = 0
1 mod 2 = 1
因为这是 (1) 和 (2) 的唯一整数解。因此,我们也会有,由(3),
0 = -0 = -(1 div 2) = (-1) div 2
由 (1) 暗示
-1 = ((-1) div 2) * 2 + ((-1) mod 2) = 0 * 2 + ((-1) mod 2) = (-1) mod 2
使(-1) mod 2 < 0
与 (2) 相矛盾。
因此,我们需要放弃 (1)、(2) 和 (3) 之间的一些属性。
一些编程语言放弃(3),并让div
向下舍入(Python、Ruby)。
在某些(罕见的)情况下,该语言提供多个除法运算符。例如,在 Haskell 中,div,mod
仅满足 (1) 和 (2),类似于 Python,我们也有 quot,rem
仅满足 (1) 和 (3)。后一对运算符将除法 向零舍入,代价是返回负余数,例如,我们有 (-1) `quot` 2 = 0
和 (-1) `rem` 2 = (-1)
。
C# 也放弃了 (2),并允许 %
返回负余数。相干地,整数除法向零舍入。 Java、Scala、Pascal 和 C,从 C99 开始,也采用这种策略。
【讨论】:
C# 放弃 (2)。 C# 中的%
运算符不是“mod”运算符。它是“余数”运算符,余数可以是负数。
@EricLippert:只是出于好奇:如果是这样,为什么运营商的 id 字符串是 op_Modulus
而不是 op_Remainder
?
@Heinzi:我无法确定任何好的理由。这只是一长串无关紧要的小错误中的一个。【参考方案2】:
浮点运算由 IEEE754 定义,考虑到数值应用,默认情况下,以非常严格定义的方式舍入到最接近的可表示值。
计算机中的整数运算没有由通用国际标准定义。语言(尤其是 C 家族的语言)授予的操作倾向于遵循底层计算机提供的任何操作。有些语言对某些操作的定义比其他语言更健壮,但为了避免在当时可用(和流行)的计算机上实现过于困难或缓慢,将选择非常接近其行为的定义。
出于这个原因,整数运算倾向于在溢出时回绕(用于加法、乘法和左移),并在产生不精确时向负无穷大圆整结果(用于除法和右移)。 这两者都是二进制补码二进制算术中在整数各自末尾的简单截断;处理极端情况的最简单方法。
其他答案讨论了语言可能与除法一起提供的余数或模数运算符的关系。不幸的是,他们倒退了。 余数取决于除法的定义,而不是相反,而模数可以独立于除法来定义 - 如果两个参数恰好是正数并且除法四舍五入,它们的结果是相同的,所以人们很少注意到。
大多数现代语言要么提供余数运算符,要么提供模数运算符,很少同时提供。库函数可以为关心差异的人提供其他操作,即余数保留被除数的符号,而模数保留除数的符号。
【讨论】:
这是一个有趣的观点,也因为您指的是模运算。如果问题是:“为什么整数除法在许多脚本语言中四舍五入?”,您认为同样的答案是否适用? @iGian 事实上,大多数语言不为了整数除法而舍入,因为我在回答中概述了原因。通常他们要么调用 CPU 的 DIV 指令并接受它所做的事情,要么调用一个子程序来实现欧几里得除法并且通常做同样的事情。一个合理的问题可能是“为什么语言 X 会四舍五入(或最接近,或接近零),而大多数其他语言却没有?” - 但您必须指定 X,而答案将特定于该语言。 那么,许多脚本语言会向下舍入整数除法,因为 CPU DIV 基本上是实现欧几里得除法? ----我越来越好奇为什么它应该被围捕。你能推荐一种语言 X 吗? @iGian 就是这样,我不知道有哪一个是你提出来的。有几个静默地将整数操作数强制转换为浮点数并执行浮点除法,除非您另有明确说明;例如。 BBC BASIC 将/
定义为 FP div,DIV
定义为向下舍入的整数。 6502 甚至没有 DIV 指令,所以这是一个子程序。
@PeterCordes:实际上“截断”仅描述在符号幅度表示中向零舍入,例如 IEEE-754 浮点。在二进制补码表示中,截断实际上是向负无穷大舍入,而 that 是您从最重要端的符号位副本移位的简单权宜之计中得到的。特别是,如果 0xFFFF 为 -1,则 0xFFFE 为 -2。【参考方案3】:
因为整数除法的含义是完整的答案包括余数。
【讨论】:
... 根据定义,余数必须始终为正数?显然,并非所有语言都如此:in C99 the remainder has the same sign as the dividend 是的,这是正确的,cmets 长度的下限让我发疯! @jjmerelo 在 Python 中,余数(%
或 divmod()
)总是与除数具有相同的符号。例如123 % -10
-> -7
@SeanFrancisN.Ballais 这显然只是一个惯例问题。 wikipedia entry on modulo operation says:当 a 或 n 为负时,幼稚的定义就会失效,编程语言在这些值的定义方式上会有所不同。【参考方案4】:
Wikipedia has a great article on this,包括历史和理论。
只要一种语言满足 (a/b) * b + (a%b) == a
的欧几里得除法属性,地板除法和截断除法都是连贯的并且在算术上是合理的。
当然,人们喜欢争论一个明显正确,另一个明显错误,但这更像是一场圣战,而不是一场明智的讨论,而且通常更多地与他们早期偏好的选择有关语言比什么都重要。他们也经常倾向于主要为他们选择的%
争论,尽管先选择/
然后再选择匹配的%
可能更有意义。
%
跟随除数的符号显然是大约 70% 的学生猜测的结果
运算符通常读作mod
或modulo
而不是remainder
。
“C 做到了”——这甚至不是真的。1
截断(如 C++):
使整数除法与 IEEE 浮点除法更加一致(在默认舍入模式下)。
更多 CPU 实现它。 (在历史的不同时期可能并非如此。)
操作符被读取为modulo
而不是remainder
(尽管这实际上与他们的观点反对)。
除法属性在概念上更多的是关于余数而不是模数。
操作符读作mod
而不是modulo
,所以它应该遵循Fortran的区别。 (这可能听起来很傻,但可能是 C99 的关键。见 this thread。)
“欧几里得”(如 Pascal—/
楼层或截断取决于符号,因此 %
永远不会是负数):
Niklaus Wirth 认为没有人会对积极的mod
感到惊讶。
Raymond T. Boute 后来辩称,您不能天真地使用其他任何规则来实现欧几里得除法。
许多语言都提供了这两者。通常——如在 Ada、Modula-2、一些 Lisps、Haskell 和 Julia 中——它们使用与 mod
相关的名称作为 Python 风格的运算符,使用与 rem
相关的名称作为 C++ 风格的运算符。但并非总是如此——例如,Fortran 调用相同的东西 modulo
和 mod
(如上文针对 C99 所述)。
我们不知道为什么 Python、Tcl、Perl 和其他有影响力的脚本语言大多选择地板。如问题中所述,Guido van Rossum 的回答仅解释了为什么他必须从三个一致的答案中选择一个,而不是为什么他选择了他所做的那个。
但是,我怀疑 C 的影响是关键。大多数脚本语言(至少在最初)都是用 C 实现的,并且从 C 中借用了它们的操作符清单。C89 的实现定义的%
显然被破坏了,不适合像 Tcl 或 Python 这样的“友好”语言。 C 称操作员为“mod”。所以他们使用模数,而不是余数。
1。尽管问题说了什么——许多人用它作为论据——C 实际上没有与 Python 和朋友有类似的行为。 C99 需要截断除法,而不是地板。 C89 既允许,也允许任一版本的 mod,因此不能保证除法属性,也无法编写可移植代码进行有符号整数除法。那只是坏了。
【讨论】:
一些 Lisps:Common Lisp 定义了mod
和rem
,参见lispworks.com/documentation/HyperSpec/Body/f_mod_r.htm 和lispworks.com/documentation/HyperSpec/Body/f_floorc.htm
@coredump 和其他一些 Lisps 使用其他名称。 MacLisp 只提供了一个,而今天的 Racket 也是如此。 Scheme 提供了三个(modulo
、remainder,
和 Wirth 风格的mod
)。我认为答案不需要与世界上每种语言的每种方言联系起来;说“一些 Lisps”使用 mod
/rem
风格的命名似乎就足够了。【参考方案5】:
正如宝拉所说,这是因为余数。
算法基于Euclidean division。
在 Ruby 中,您可以编写以下代码以保持一致性重建红利:
puts (10/3)*3 + 10%3
#=> 10
在现实生活中也是如此。 10个苹果和3个人。好的,您可以将一个苹果切成三分之二,但要超出设定的整数。
负数也保持一致性:
puts (-10/3)*3 + -10%3 #=> -10
puts (10/(-3))*(-3) + 10%(-3) #=> 10
puts (-10/(-3))*(-3) + -10%(-3) #=> -10
商总是向下取整(沿负轴向下),提示如下:
puts (-10/3) #=> -4
puts -10%3 #=> 2
puts (10/(-3)) #=> -4
puts 10%(-3) # => -2
puts (-10/(-3)) #=> 3
puts -10%(-3) #=> -1
【讨论】:
所以除法和取模运算必须是一致的,这很清楚。但是您可以选择向上或向下舍入,只要这些等式成立,它仍然保持一致。 @jjmerelo,我同意你的观点,数学上你可以选择不同的规则,# 10 / 3 = 4 # 10 % 3 = -2 # 3 * 4 - 2 = 10
。但这不适用于不存在-2
的自然数集。
@iGian 您缺少的答案是,有不止一种方法可以在不改变正数结果的情况下保持负数的一致性。【参考方案6】:
此答案解决了其他(优秀)答案未明确解决的问题的子部分。您注意到:
如果你选择四舍五入,你也可以保持连贯性。
其他答案解决了向下舍入(向 -∞ 方向舍入)和截断(向 0 方向舍入)之间的选择,但没有比较向上舍入(向 ∞ 方向)。
(accepted answer 涉及性能原因,更喜欢在二进制补码机器上进行四舍五入,这也适用于四舍五入。但还有更重要的语义原因以避免四舍五入。)
这个答案直接解决了为什么四舍五入不是一个很好的解决方案。
总结打破了小学的期望
以previous answer's 中的示例为基础,通常非正式地说这样的话:
如果我把十四颗弹珠平均分给三个人,每个人得到四颗弹珠,剩下两颗弹珠。
确实,这是第一次教多少学生除法(在介绍分数/小数之前)。学生可能会写14 ÷ 3 = 4 remainder 2
。由于它引入得这么早,我们真的希望我们的 div
运算符保留这个属性。
或者,更正式地说,在top-voted answer 中讨论的三个属性中,第一个 ((a div b) × b + (a mod b) = a
) 是迄今为止最重要的。
但是四舍五入破坏了这个属性。如果div
向上取整,则14 div 3
返回5
。这意味着上面的等式简化为15 + (13 mod 4) = 13
- 而对于mod
的任何 定义,情况并非如此。同样,不太正式/小学的方法也不走运——或者至少需要引入负数弹珠:“每个人得到 5 个弹珠,剩下一个负数”。
(如上例所示,舍入到最接近的整数也会破坏属性。)
因此,如果我们想保持基本的期望,我们不能四舍五入。通过四舍五入,您在问题中链接的coherency argument 足以证明四舍五入是合理的。
【讨论】:
以上是关于为啥整数除法在许多脚本语言中舍入?的主要内容,如果未能解决你的问题,请参考以下文章