我如何获得负股息的正模数
Posted
技术标签:
【中文标题】我如何获得负股息的正模数【英文标题】:How do I get a positive modulo on a negative dividend 【发布时间】:2016-11-29 08:29:12 【问题描述】:我正在尝试在[0 ... n-1]
范围内获得模n (0
指令idiv n
将EDX:EAX(64位)除以n,然后离开
EAX=商,EDX=余数。 (n
是我代码中的一个寄存器)
我的问题是当 EDX:EAX 的内容为负数时,我在 EDX 中得到一个负数结果。
我找到的最简单的解决方案是:
cdq ; extend EAX sign bit to EDX
idiv n ; edx = (possibly neg.) remainder
add edx, n
mov eax, edx ; eax = remainder + n
cdq
idiv n ; edx = positive remainder
是否有更清洁/更简单/更快的方法来获得正余数?
【问题讨论】:
称为余数。商是除法的结果。 现在我看到你没有明确指定n
为正数,我从 "edx:eax 为负数 => 负余数" 中扣除了一个,这可以只发生在正面n
。 (“非负数”也是如此,因为n==0
会更快失败:))。
@Ped7g,感谢您的 cmets。我将编辑并将“商”更改为“余数”。关于 n,我确实提到了 0
【参考方案1】:
-5 mod 3 = -2(余数,-1 商)
修补余数:-2 + 3 = +1,这就是你想要的,对吧?
商是 -1 - 1 = -2。
验证:-2 * 3 + +1 = -5
cdq ; extend EAX sign bit to EDX
idiv n ; edx = (possibly neg.) remainder
mov eax, edx ; eax = copy of remainder
add edx, n ; negative remainder modified to positive one (for --quotient)
; when going from negative to positive, CF will be set (unsigned overflow)
cmovc eax,edx ; when CF, load eax with modified remainder
; eax = to-be-positive-adjusted remainder
我没有在调试器中验证,只是醒了,所以先测试一下。
【讨论】:
是的,至少看起来适合正除数。从负到正的环绕设置 CF。 (虽然这是 not 签名溢出,所以我建议将其称为“环绕”而不是(overflow)
以避免混淆。)
我认为这会破坏较大的正余数:如果 n + remainder
携带,那么您将得到 n + remainder
而不是正余数。
@PeterCordes 一直在考虑这个问题。他使用的是idiv
,所以n
的签名是32b。然后 MAX_N = 0x7F...FF
。最大余数为0x7F...FE
。 0x7F...FF
+ 0x7F...FE
= 0xFF...FD
,CF = 0。因此,只要 n
签名且为正,它就应该可以正常工作。编辑:我也不确定那个“环绕”的措辞,add edx,n
确实会产生经典的无符号add
溢出,当你在没有签名上下文的情况下查看它时。我至少添加了“未签名”,但在这种情况下,“溢出”对我来说感觉合适吗?可能取决于您如何阅读代码,如果您使用负数+n 或 uint+n?
啊,好点。我同意,MAX_INT + MAX_INT
不携带,并且 OP 说除数是非负的,所以没有问题。
事实证明,给定正确的 C 输入,我们可以让 clang 发出这个 asm(请参阅我的答案)。 gcc 仍然想使用 CMP 而不是从 ADD 中读取 CF。除了我还发现了一篇有趣的论文,以及这种模在数学上称为什么之外,不值得一个完整的答案。【参考方案2】:
产生非负模的除法称为Euclidean division。
有关 C 实现和更多背景信息,请参阅Division and Modulus for Computer Scientists。 (也相关:What's the difference between “mod” and “remainder”?)。
这是一种特殊情况,我们知道除数是正数,这允许 Ped7g 建议的更快实现。它可能是最佳的或至少接近它。
有趣的是,我们可以用 C 语言编写它,编译成与 Ped7g 的手写版本相同的 asm(或接近它,取决于 gcc 与 clang)。见on the Godbolt compiler explorer。
// https://***.com/questions/40861023/how-do-i-get-a-positive-modulo-on-a-negative-dividend
// uses unsigned carry to detect when a negative 2's complement integer wrapped to non-negative
long modE_positive_divisor_2s_complement( long D, long d )
long r = D%d;
unsigned long tmp = (unsigned long)r + (unsigned long)d;
// detect carry by looking for unsigned wraparound:
// that means remainder was negative (not zero), so adding the (non-negative) divisor is what we need
r = (tmp < (unsigned long)r) ? (long)tmp : r;
return r;
使用 clang3.9 -O3,我们得到:
mov rax, rdi
cqo
idiv rsi
add rsi, rdx
cmovae rsi, rdx
mov rax, rsi
ret
gcc6.2 在 CMOV 之前使用了一个额外的 CMP :(
在 Godbolt 上,我包含了原始的通用 C 函数和一个版本,它告诉编译器假设为正值 d
(使用 __builtin_unreachable()
)。对于后者,gcc 的效果与此差不多,使用 test rdx,rdx
/ cmovs
进行条件添加。
对于 32 位和 64 位 long
(使用 EAX 而不是 RAX 等),它的工作方式相同,因此如果您关心性能并且不需要 64 位整数,请使用 int32_t
。 (idiv r64
比 idiv r32
慢得多)。
【讨论】:
以上是关于我如何获得负股息的正模数的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 XML 获得负半径 Android Round Corner