看起来,每当我将一个负整数除以正整数时,我需要将其向下取整(朝着 -inf),而不是朝着 0。但是 C# 和 C++ 都会朝着 0 取整。
所以我想我需要一个 DivideDownward() 方法。我可以用几行代码编写它,并测试负数等等,但我的想法似乎有些笨拙。所以我在想是否我遗漏了什么,是否您有一种“优雅”的方法来朝负数方向取整。
看起来,每当我将一个负整数除以正整数时,我需要将其向下取整(朝着 -inf),而不是朝着 0。但是 C# 和 C++ 都会朝着 0 取整。
所以我想我需要一个 DivideDownward() 方法。我可以用几行代码编写它,并测试负数等等,但我的想法似乎有些笨拙。所以我在想是否我遗漏了什么,是否您有一种“优雅”的方法来朝负数方向取整。
您可以通过这样做来消除任何分支:
inline int DivideRoundDown(int a_numerator, int a_denominator)
{
return (a_numerator / a_denominator) + ((a_numerator % a_denominator) >> 31);
}
a/b
与a%b
具有相同的符号 - 不确定是否由标准保证,但我认为所有现代编译器/硬件都是如此 - 还依赖于有符号右移是算术的(如果我记得正确的话,即使是今天 - 是的,我知道我晚了十多年,但可能还有其他晚读者... - 尽管所有我知道的编译器/硬件都是算术右移) - 最后依赖于int
具有32位,这绝对不是每台机器的情况(有较小int
的MCU)。 - undefined>> sizeof(int) * CHAR_BIT - 1
可以轻松满足最后一个要求。) - undefineddouble
,并在将商转换回整数时使用FP舍入模式向负无穷方向舍入。今天的浮点运算单元非常快,实际上可能比整数单元更快地进行除法运算;要确保,您需要进行测量。static inline
. 这样,当你的解决方案在5年内交付新的高科技硬件时被证明不够优秀时,你只需更改一个地方。假设b始终为正数,则以下是一种简单的解决方案:
inline int roundDownDivide(int a, int b) {
if (a >= 0) return a/b;
else return (a-b+1)/b;
}
var res = a / b - (a % b < 0 ? 1 : 0);
a
为负数的情况。所以我想一个通用版本应该是:a / b - (a < 0 && a % b != 0 ? 1 : 0)
。 - Matthew Flaschenb==0
,否则它们要求(a/b)*b+a%b==a
,即使/
和%
返回的实际值对于负操作数是实现定义的。如果内置的/
向-无穷大舍入而不是向0舍入,则您的版本将失败。 - Chris Doddb
为负数,这将产生错误的结果。例如,-1 / -2
计算出的结果是 -1
而不是 0
。 - Mr. Smithb
的版本。 - Yakov Galkaa = b * (a / b) + a % b
a/b
和 a%b
的确切值未定义[0],这也可以被利用在许多语言和平台上计算所需的结果,使用以下(等效的)代码:int divF(int a, int b) { return a / b - (a % b < 0); }
int sign(int x) { return (x > 0) - (x < 0); }
int divF2(int a, int b) { return a / b - (sign(a % b) == -sign(b)); }
int divE(int a, int b) {
int c = a % b < 0;
return a / b + (b < 0 ? c : -c);
}
-O3
生成无分支代码。然而,在二进制补码架构上,以下内容可能会稍微快一些:int divE2(int a, int b) { return a / b + (-(a % b < 0) & (b < 0 ? 1 : -1)); }
divF:
cdq
idiv esi
sar edx, 31
add eax, edx
divF2:
cdq
idiv esi
xor ecx, ecx
test edx, edx
setg cl
sar edx, 31
add edx, ecx
xor ecx, ecx
test esi, esi
setg cl
shr esi, 31
sub esi, ecx
xor ecx, ecx
cmp edx, esi
sete cl
sub eax, ecx
Chazz:
cdq
idiv esi
test edx, edx
cmove edi, esi
xor edi, esi
sar edi, 31
add eax, edi
divE:
cdq
idiv esi
mov ecx, edx
shr ecx, 31
sar edx, 31
test esi, esi
cmovs edx, ecx
add eax, edx
divE2:
cdq
idiv esi
sar edx, 31
shr esi, 31
lea ecx, [rsi + rsi]
add ecx, -1
and ecx, edx
add eax, ecx
simple truncating division:
2464805950: 1.90 ns -- base
euclidean division:
2464831322: 2.13 ns -- divE
2464831322: 2.13 ns -- divE2
round to -inf for all b:
1965111352: 2.58 ns -- Chazz
1965111352: 2.64 ns -- divF2
1965111352: 5.02 ns -- Warty
round to -inf for b > 0, broken for b < 0:
1965143330: 2.13 ns -- ben135
1965143330: 2.13 ns -- divF
1965143330: 2.13 ns -- Tomas
4112595000: 5.79 ns -- runevision
round to -inf, broken for b < 0 or some edge-cases:
4112315315: 2.24 ns -- DrAltan
1965115133: 2.45 ns -- Cam
1965111351: 7.76 ns -- LegsDrivenCat
使用clang -O3
编译,在FreeBSD 12.2上运行,i7-8700K CPU @ 3.70GHz。第一列是生成相同结果的校验和分组算法。base
是用于测量测试开销的最简单的截断除法。
测试平台:
static const int N = 1000000000;
int rng[N][2], nrng;
void push(int a, int b) { rng[nrng][0] = a, rng[nrng][1] = b, ++nrng; }
SN_NOINLINE void test_case(int (*func)(), const char *name) {
struct timespec t0, t1;
clock_gettime(CLOCK_PROF, &t0);
int sum = func();
clock_gettime(CLOCK_PROF, &t1);
double elapsed = (t1.tv_sec - t0.tv_sec)*1.e9 + (t1.tv_nsec - t0.tv_nsec);
printf("%10u: %5.2f ns -- %s\n", sum, elapsed/N, name);
}
#define ALL_TESTS(X) X(base) X(divF) X(divF2) X(divE) X(divE2) X(ben135) X(DrAltan) X(Cam) X(Tomas) X(Warty) X(Chazz) X(runevision) X(LegsDrivenCat)
#define LOOP_TEST(X) \
SN_NOINLINE int loop_##X() { \
int sum = 0; \
for(int i = 0; i < N; ++i) sum += X(rng[i][0], rng[i][1]); \
return sum; \
} \
/**/
ALL_TESTS(LOOP_TEST)
int main() {
srandom(6283185);
push(INT_MAX, 1); push(INT_MAX, -1); push(INT_MIN, 1);
push(INT_MAX, 2); push(INT_MAX, -2); push(INT_MIN, 2); push(INT_MIN, -2);
while(nrng < N) {
int a = random() - 0x40000000, b;
do b = (random() >> 16) - 0x4000; while(b == 0);
push(a,b);
}
#define CALL_TEST(X) test_case(loop_##X, #X);
ALL_TESTS(CALL_TEST)
}
注意:此文章针对a=-1的输入结果不正确,请参考其他答案。
[c++]
这可能就是你所说的“笨拙”,但这是我想出来的方法。
int divideDown(int a, int b){
int r=a/b;
if (r<0 && r*b!=a)
return r-1;
return r;
}
if (a<0 && b>0)
if (r<=0 && r*b!=a)
吧? - Chris Burt-Brownint floor_div2(int a, int b)
{
int rem = a % b;
int div = a/b;
if (!rem)
a = b;
int sub = a ^ b;
sub >>= 31;
return div+sub;
}
floor_div2(INT_MIN,-1)
在a/b
方面会导致问题。当int
的大小不是32位时会失败。它依赖于二进制补码——这通常是这些天的情况。 - chux - Reinstate MonicaINT_MIN/-1
是_未定义行为_。它能在你的情况下正常工作并不代表语言规定它对所有情况都有效。 - chux - Reinstate Monicafloor_div2
的行为与内置除法相同,这正是我期望这样一个函数所做的。 - Yakov GalkaMath.Floor((double)a/(double)b)
如果你需要它作为int类型,在此之后进行转换
(int)Math.Floor((double)a/(double)b)
decimal
是最慢的数值类型(参见http://msdn.microsoft.com/en-us/library/xtba3z33.aspx),在这里并没有什么用处。使用`float`或`double`会更加合适。 - Matthew Flaschen阿特兰博士的解决方案是最常见的答案,但如果您知道a不会小于某个固定数字,则可以添加最小的b的倍数以使其为正值,然后调整结果。例如,如果a始终大于等于-10 * b,则可以说:
return (a + 10 * b) / b - 10
或者,如果您只想在 a 为正数或0时获得正确的结果,并在 a 为负数时获得一些负结果(例如,用于测试结果是否>= 0,而不包括范围 a = [-1; -b +1]),您可以执行以下操作
return (a + b) / b - 1;
对于 [-1,-b+1] 和 [-b;-2*b+1],该函数将返回-1。但是,如果您只是想确定除法的结果是否为正,则这并不重要。实际上,我们只是更改四舍五入方式,使其向 -1 而不是 0 的方向取整。