除法有负的红利,但四舍五入到负无穷

Division with negative dividend, but rounded towards negative infinity?

本文关键字:四舍五入 除法      更新时间:2023-10-16

考虑以下代码(c++ 11):

int a = -11, b = 3;
int c = a / b;
// now c == -3

c++ 11规范规定带有负被除数的除法向零四舍五入。

对于有一个运算符或函数来做除法,舍入到负无穷是非常有用的(例如,迭代范围时与正红利的一致性),所以在标准库中是否有一个函数或运算符来做我想要的?或者可能是编译器定义的函数/内在的,在现代编译器中?

我可以自己写,比如下面的(只适用于正除数):

int div_neg(int dividend, int divisor){
    if(dividend >= 0) return dividend / divisor;
    else return (dividend - divisor + 1) / divisor;
}

但是它不会像我的意图那样描述,并且可能不像标准库函数或编译器固有的(如果存在的话)那样优化。

我不知道它有什么内在的。我将简单地对标准除法进行回顾性修正。

int div_floor(int a, int b)
{
    int res = a / b;
    int rem = a % b;
    // Correct division result downwards if up-rounding happened,
    // (for non-zero remainder of sign different than the divisor).
    int corr = (rem != 0 && ((rem < 0) != (b < 0)));
    return res - corr;
}

请注意,它也适用于c99之前和c++ 11之前,即没有标准化的四舍五入除到零。

这是另一种可能的变体,适用于正除数和任意股息。

int div_floor(int n, int d) {
    return n >= 0 ? n / d : -1 - (-1 - n) / d;
}

说明:在n为负的情况下,对(-1 - n) / dq,对满足0 <= r < d的某个r-1 - n = qd + r。重新排列得到n = (-1 - q)d + (d - 1 - r)。很明显,0 <= d - 1 - r < d,所以d - 1 - r是除法运算的余数,-1 - q是商。

请注意,这里的算术运算都是安全的,不会发生溢出,无论有符号整数的内部表示(2的补数,1的补数,符号大小)如何。

假设有符号整数的补码表示为2,一个好的编译器应该将两个-1-*操作优化为按位求负操作。在我的x86-64机器上,该条件的第二个分支被编译成以下序列:

notl    %edi
movl    %edi, %eax
cltd
idivl   %esi
notl    %eax

标准库只有一个函数可以用来做你想做的事情:floor。您需要的除法可以表示为floor((double) n / d)。然而,这假设double有足够的精度来准确地表示nd。如果不是,那么这可能会引入舍入错误。

就我个人而言,我会使用自定义实现。但是你也可以使用浮点版本,如果它更容易阅读,并且你已经验证了你调用它的范围的结果是正确的。

c++ 11有一个std::div(a, b),它返回a % ba / b的struct,其中包含remquot成员(即剩余和商原语),并且现代处理器只有一条指令。c++ 11支持截断除法。

要对余数和商进行除法,可以这样写:

// http://stackoverflow.com/a/4609795/819272
auto signum(int n) noexcept
{
        return static_cast<int>(0 < n) - static_cast<int>(n < 0);
}
auto floored_div(int D, int d) // Throws: Nothing.
{
        assert(d != 0);
        auto const divT = std::div(D, d);
        auto const I = signum(divT.rem) == -signum(d) ? 1 : 0;
        auto const qF = divT.quot - I;
        auto const rF = divT.rem + I * d;
        assert(D == d * qF + rF);
        assert(abs(rF) < abs(d));
        assert(signum(rF) == signum(d));
        return std::div_t{qF, rF};
}

最后,在您自己的库中也可以方便地使用欧几里得除法(其余数总是非负的):

auto euclidean_div(int D, int d) // Throws: Nothing.
{
        assert(d != 0);
        auto const divT = std::div(D, d);
        auto const I = divT.rem >= 0 ? 0 : (d > 0 ? 1 : -1);
        auto const qE = divT.quot - I;
        auto const rE = divT.rem + I * d;
        assert(D == d * qE + rE);
        assert(abs(rE) < abs(d));
        assert(signum(rE) != -1);
        return std::div_t{qE, rE};
}

有一篇微软的研究论文讨论了三个版本的优缺点。

当两个操作数都为正数时,/运算符执行分层除法。

当两个操作数都为负时,/运算符执行分层除法。

当其中一个操作数为负时,/运算符执行上限除法。

对于最后一种情况,当恰好有一个操作数为负且没有余数时,商可以调整(没有余数时,下限除法和上限除法的工作相同)。

int floored_div(int numer, int denom) {
  int div = numer / denom;
  int n_negatives = (numer < 0) + (denom < 0);
  div -= (n_negatives == 1) && (numer % denom != 0);
  return div;
}