正如许多人所指出的,在当前的 C 和 C++ 标准中,x % n 不再是为 x 和 n 的任何值定义的实现。在 x / n 未定义 [1] 的情况下,这是未定义的行为。此外,x - y 在整数溢出的情况下是未定义的行为,如果x 和y 的符号可能不同,则可能发生这种情况。
因此,通用解决方案的主要问题是避免整数溢出,无论是在除法还是减法中。如果我们知道x 和y 是非负的并且n 是正的,那么溢出和被零除是不可能的,我们可以自信地说(x - y) % n 是定义的。不幸的是,x - y 可能是负数,在这种情况下,% 运算符的结果也是如此。
如果我们知道n 是正数,就很容易纠正负数的结果;我们所要做的就是无条件地添加n 并执行另一个modulo 操作。这不太可能是最好的解决方案,除非你有一台除法比分支快的计算机。
如果有条件加载指令可用(现在很常见),那么编译器可能会很好地处理以下代码,这些代码可移植且定义明确,受x,y ≥ 0 ∧ n > 0 的约束:
((x - y) % n) + ((x >= y) ? 0 : n)
例如,gcc 为我的核心 I5 生成此代码(尽管它的通用性足以在任何非古生代英特尔芯片上工作):
idivq %rcx
cmpq %rsi, %rdi
movl $0, %eax
cmovge %rax, %rcx
leaq (%rdx,%rcx), %rax
这是令人愉快的无分支。 (条件移动通常比分支快很多。)
另一种方法是(除了需要编写函数sign):
((x - y) % n) + (sign(x - y) & (unsigned long)n)
如果 sign 的参数为负数,则其全为 1,否则为 0。符号的一种可能实现(改编自 bithacks)是
unsigned long sign(unsigned long x) {
return x >> (sizeof(long) * CHAR_BIT - 1);
}
这是可移植的(定义了将负整数值转换为无符号),但在缺乏高速移位的架构上可能会很慢。它不太可能比以前的解决方案更快,但是 YMMV。 TIAS。
对于可能整数溢出的一般情况,这些都不会产生正确的结果。处理整数溢出非常困难。 (一个特别烦人的情况是n == -1,尽管您可以对其进行测试并返回 0 而无需使用任何%。)此外,您需要确定您对负n 的模结果的偏好。我个人更喜欢 x%n 为 0 或与 n 具有相同符号的定义——否则你为什么要使用负除数——但应用程序不同。
如果n 不是-1 并且n + n 不溢出,Tom Tanner 提出的三模解决方案将有效。如果x 或y 是INT_MIN,n == -1 将失败,如果n 是INT_MIN,则使用abs(n) 而不是n 的简单修复将失败。 n 具有较大绝对值的情况可以用比较来代替,但是有很多极端情况,并且由于标准不需要 2 的补码运算,因此变得更加复杂,因此不容易预测什么是极端情况是 [2]。
最后一点,一些诱人的解决方案不起作用。你不能只取(x - y)的绝对值:
(-z) % n == -(z % n) == n - (z % n) ≠ z % n(除非z % n 恰好是n / 2)
而且,出于同样的原因,您不能只取模结果的绝对值。
此外,您不能只将(x - y) 转换为无符号:
(unsigned)z == z + 2<sup>k</sup> (for some k) if z < 0
(z + 2<sup>k</sup>) % n == (z % n) + (2<sup>k</sup> % n) ≠ z % n 除非(2<sup>k</sup> % n) == 0
[1] 如果n==0,x/n 和 x%n 都是未定义的。但是如果x/n 是“不可表示的”(即存在整数溢出),x%n 也未定义,这将发生在二进制补码上
如果x 是最负的可表示数字且n == -1,则机器(即您关心的所有机器)。很清楚为什么在这种情况下 x/n 应该是未定义的,但在 x%n 的情况下应该是未定义的,因为该值(数学上)是 0。
[2] 大多数抱怨预测浮点运算结果困难的人并没有花太多时间尝试编写真正可移植的整数运算代码:)