我如何获得负股息的正模数

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...FE0x7F...FF + 0x7F...FE = 0xFF...FDCF = 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 r64idiv r32 慢得多)。

【讨论】:

以上是关于我如何获得负股息的正模数的主要内容,如果未能解决你的问题,请参考以下文章

如何解决可能的乘法溢出以获得正确的模数运算?

NEXO代币持有者获得20,428,359.89美元股息

如何获得正数的负二进制数

如何使用 XML 获得负半径 Android Round Corner

如何获得一个良好的二元分类深度神经模型,其中负数据更多位于数据集上?

如何将模数用于浮点/双精度?