这段话是否正确?「取余这个运算,只有 Python 是对的。当初 C 这个老师教错了,那么一大票学生也就只敢跟着老师错。只有 Python 敢于站出来坚持正确答案。」

3 天前
 cnbatch

今天在看一篇公众号文章《性能之王:最快的编程语言》,发现评论区有这么一段对话:

然后我找了下在 stackexchange 的真实提问:

https://math.stackexchange.com/questions/623449/negative-number-divided-by-positive-number-what-would-be-remainder

从回答来看,C 和 Python 的两种做法在数值计算上都是成立的。两种做法的区别在于是否允许余数为负数,或者说,符号该不该与原数值相同。

不允许余数出现负数的,是目前广泛使用的欧几里得除法。

所以“数学洁癖”会认为负值余数是错的?

3901 次点击
所在节点    C
50 条回复
mightybruce
3 天前
这个取余运算的确在数学上是没有负数,这个的确没有问题。毕竟搞计算机的很多数学水平一般,这也很正常。

读计算机科学里面一些分支的博士,就会发现基本没有本科学计算机的了, 尤其是密码学、数值分析这些。
mark2025
3 天前
py 真是性能亡者~
codehz
3 天前
本身就是定义学的问题,太过纠结这种东西没意义。。。
连 0 是否是自然数,(类似数组下标从 0 还是从 1 开始)都可以有好多说法
以及 1 是不是素数
我建议从实用主义出发,别去想这种东西哪个“更合理”
mightybruce
3 天前
数学要求体系必须是自洽的, 不像计算机学科,当然数学有一点不行,就是数学符号乱飞,不同体系下的同一个数学符合意思都不一样。
计算机本身运算你可以认为都是在有限群上的, 所以取余后是正数是没错的, 另外负数的平方根在计算机中是二次剩余也是正数。
mightybruce
3 天前
@mark2025 python 也不是非常拉跨,pypy 是 python jit 解释器, 只不过 python 默认的 cpython 那是运行效率低。
Eureka0
3 天前
数论里面 -1 与 2 是模 3 同余的,属于同一个同余类,取余等于多少,就是一个怎么选同余类代表数的定义问题,Python 选最小非负整数集(最小剩余系),C 选 {-[n/2], ..., [n/2]+1},数学上其实都没有问题
dnfQzjPBXtWmML
3 天前
C 不是给无能巨婴用的语言,溢出、越界、CPU 的特性都开放给你,不懂搞出问题了是你自己的问题。
觉得 CPU 指令设计有问题可以去 intel/amd 门口举牌子。
从这个上面能总结出 XXX 的多少沾点智慧。
FalconD
3 天前
这就是个定义问题,下面是 Haskell 标准库的结果:
quotRem 3 2 = (1, 1)
quotRem 3 -2 = (-1, 1)
quotRem -3 2 = (-1, -1)
quotRem -3 -2 = (1, -1)
divMod 3 2 = (1, 1)
divMod 3 -2 = (-2, -1)
divMod -3 2 = (-2, 1)
divMod -3 -2 = (-2, -1)
和 LLM 的总结一致
rem: The result has the same sign as the dividend.
mod: The result has the same sign as the divisor, or is zero
liprais
3 天前
@codehz 大专?
FalconD
3 天前
在这种意义下 C 的行为没有问题 因为 -1/3 + -1%3 == -1
C 只是约定 / 代表 quotation, % 代表 reminder
zzzsy
3 天前
除法又不是只有欧几里得除法一种
majula
3 天前
编程语言提供的基本数学运算是方便开发者编写程序的,而不是用来进行数学研究的。要想做后者,应当使用专门的软件(比如 Scilab )

非得要求编程语言中的概念在数学上“正确”,无异于耍流氓

----

题外话,有“数学洁癖”的人最常吐槽的是众多编程语言的“变量”,完全跟数学上的“变量”是不同的东西

不过在这一点上 C 语言并没有中枪,因为 C 语言并没有“变量”这一概念(翻一翻标准手册,会发现 variable 这个词唯一出现在的地方是 VLA )
angrylid
3 天前
无奖竞猜:有一种主流编程语言 0/0 不会抛出异常且可以得到合法返回值
ZE3kr
3 天前
错的多了就成标准了。HTTP 里 referrer 错误拼写成了 referer ,但现在用的都是错误的拼写
secondwtq
3 天前
这些语言的行为在它们自己的体系里是自洽的——比如 C 的浮点数转整数会直接把浮点部分切掉,而 C 的除法,商也是把浮点部分切掉,然后根据此算出余数。如果用传统香烟,啊不传统余数,那同时算出的商和余数会不满足 商*除数+余数=被除数 这一基本原则,这个问题显然更严重。
注意这个行为是 C99 之后才有的,之前没有定义,不过 C99 之前标准库里定义了 div() 函数,可以同时算出商和余数,是一直遵循这个行为的。主流实现比如 x86 的 idiv 指令应该一直都是这样。

C 标准库对浮点数还定义了 fmod() 和 remainder() 两个函数,两个采取了不同的定义,remainder() 函数对应的是 IEEE 754 标准定义的 remainder 操作。fmod() 函数我没有在标准里找到对应。

Python 虽然浮点强转整数也是切,但是貌似实际用得不多,默认的 / 不能整除时直接给浮点,// 和 % 也是一致的。

至于拿计算机语言强行追求贴合数学定义我觉得大可不必,光浮点数就很头疼。等下个 IEEE 754 标准更新之后,可能会有很多符合该标准的实现,但是可能大多数人不会用。
coderluan
3 天前
作者说只有 python 是对的,其他语言是错的,从数学的角度我并不反对。

但是作者说其他编程语言是因为 C 语言这么做,所以才跟着这么做的。我感觉作者有点太不看不起其他编程语言了吧,那其他语言和 C 语言不一致情况怎么解释,其他语言这会又不怕了吗?这就是明显的拉踩行为啊。
cooltechbs
3 天前
谈数学怎么能不提 Fortran ,Fortran 是怎么处理的(我真的不懂,真心发问)?
而其他语言“错”的根源肯定也不是 C ,而是汇编/机器码。这方面 ARM 、MIPS 又是怎么处理的?
vvhy
3 天前
两种定义在数学上都是自洽的
secondwtq
3 天前
我记得这个问题我很久之前折腾过,不过具体怎样忘了(当时也没搞 Numerics ),我翻了一下记录,有这么一篇论文:
dl.acm.org/doi/pdf/10.1145/128861.128862 The Euclidean Definition of the Functions div and mod
刚才搜到了这个
github.com/WebAssembly/design/issues/250 Semantics of signed integer divide and remainder · Issue #250 · WebAssembly/design · GitHub
根据这个 thread ,最早用 truncating division 的是 Fortran ,原因是早期机器上多不使用 2's complement 表示,truncating division 更好实现,C 出于和 Fortran 兼容的考虑,最后也用了 truncating division 。但是现在的 2's complement 表示上,Euclidean division 可能更好实现(见上面论文,另外两个都引用了 Guy Steele 的 Arithmetic Shifting Considered Harmful ,不过这个我还没看)。但是 truncating division 作为前 2's complement 时代的习惯保留下来了。

所以可能还真不是 C 带的头。至于是不是真的 Fortran 先干的我也不确定( Fortran 66 标准里面我没找到,77 里面倒是有,不过那时候已经有原始的 C 了),但是考古只考到 C 大概是不合格的,就算暴论也没上面那个 thread 有活。
另外上面的“好实现”指得是用 ASR 操作来模拟,硬件除法器有自己的算法,我还没看过。
geelaw
3 天前
C 语言规定 a / b 的值 q 是 a 除以 b 向零取整,而 a % b 是满足 a = qb + r (带余除法恒等式)惟一的 r 。
数论中常见的定义是 0 <= r < |b|,此时 q 的数值并不是 a 除以 b 向零取整,而是向下取整,比如

C 语言:
-1 = 0*3 + (-1)
1 = 0*(-3) + 1

数论:
-1 = (-1)*3 + 2
1 = (-1)*(-3) + 2

带余除法恒等式相当重要且自然,如果丧失它则扩展欧几里得算法 [给定 a, b 计算 x, y 使 ax+by=(a,b)] 会很难写对。以下三者不可兼得:

1. 带余除法恒等式
2. 对一切 a 不是 int 最小值且 b 不是 0 ,成立 -(a / b) == (-a) / b 且 -a / b == 0 - a / b ,即“向零取整”
3. a % b 永远是非负数

值得注意的是 Python 也没有完全采用数论中常见的定义,因为 Python 里 a % b 的符号是 0 或者和 b 相同(整数的情况),而不是永远非负。

C 和 Python 都不是“常见数论教材”纯粹的。数学上对余数的选择没有某种必然的对错,通常选 (-b, b) 里的任何数都不会导致常见的算法(如欧几里得算法)无法继续。

C 语言选择向零取整、保持带余除法恒等式,虽然 a % b 可能有负数,但是保证了

-a/b
(-a)/b
(0-a)/b

-(a/b)
0-(a/b)
0-a/b

的计算结果都相同(假设 a 不是 int 最小值且 b 不是 0 )。而在 Python 里面,对于整数 a,b ,表达式

-a//b
(-a)//b
(0-a)//b



-(a//b)
0-(a//b)
0-a//b

的两组结果分别相同,但组间可以不同,不同当且仅当 a/b 是负非整数。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1093235

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX