c# 的取负数_关于C#:如何对负数*向下*进行整数除法?

似乎每当我将负整数除以正整数时,我都需要将其舍入(向-inf方向),而不是朝0方向取整。但是C#和C ++都朝着0取整。

所以我想我需要一个DivideDownward()方法。 我可以用几行代码来测试否定性,等等,但是我的想法似乎很笨拙。 因此,我想知道我是否缺少某些东西,以及您是否有一种"优雅"的方式可以将负除法向下舍入。

举例说明所需的行为。

例? 好,我需要(-5)/ 3才能产生-2,而不是-1。 但是我真的不认为该示例为(IMO)明确的问题添加了任何内容。

Whenever I divide a negative int by a positive int, I need it to round down.

妈的,不是吗?克努斯(Knuth)写道,为什么这是正确的处理方法,但是我们仍然使用传统的整数硬件。

如果您可以承受精度损失,最简单,最干净的方法是将32位整数转换为64位double,并在将商返回时使用FP舍入模式向负无穷大舍入。到整数。当今的浮点单位非常快,实际上可能比整数单位要快。当然,您必须进行测量。

如果您需要完整的64位整数精度,那么作为编译器编写者,我可以通过执行两个条件分支来解决此问题,以便最终划分出幅度,然后获得正确的符号。但这是前一阵子,当时条件分支的价格比分红便宜。在当今的硬件上,我必须先进行实验,然后才能推荐一些东西。

原则上,您可以使用旧的英特尔80位浮点数来在64位整数上使用浮点数技巧,但这是非常不可移植的,我不相信英特尔会一直保持该单位的快速发展。如今,浮点速度以SSE为单位。

寻找其他技巧的地方包括汉克·沃伦(Hank Warren)的书《黑客的喜悦》(Hacker's Delight)(我的著作正在编写中)和用于标准ML的MLton编译器,该编译器需要整数除法以向负无穷大舍入。

无论做什么,当您适应它时,如果使用的是C ++或C99,请将除法例程粘贴到.h文件中,并将其设置为static inline。这样,当您的解决方案对于5年内交付的新型Whizbang硬件而言不是最理想的解决方案时,您就可以更改它。

我以为int32-> double不会失去任何精度。但是我只是优化了一些fp代码,其中fstp ops(接近0的数字)占用了我的大部分CPU时间。也许无关紧要,但是我暂时对不必要的fp感到厌烦。 ;)

您可以通过执行以下操作摆脱任何分支:

inline int DivideRoundDown(int a_numerator, int a_denominator)

{

return (a_numerator / a_denominator) + ((a_numerator % a_denominator) >> 31);

}

假设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);

这可能会编译成很多指令,但仍可能比使用浮点数更快。

OP没有提到C,但是仍然值得注意的是,由于实现定义的百分比,因此不能保证C89可以正常工作。我认为您可以通过更改为a % b != 0来概括它。

@Matthew:那很棘手。但是,如果a % b返回的数字大于零,则需要返回0(这就是为什么我在C#中需要

@汤姆,好点。我在想a是否定的。所以我想一个广义的版本是:a b - (a < 0 && a % b != 0 ? 1 : 0)

我认为给出的答案取决于实现,但是Matthews的修改显得简洁可靠。如果是答案,请选中它。谢谢大家)!

@Matthew:实际上,这保证了C89和C99都可以工作-即使和%返回的实际值是为负操作数定义的实现,它们也要求(ab)*b + a%b == a除非b == 0。如果内建/朝-inf而不是0舍入,则您的版本将失败。

@Chris:至少由MS文档,在C#和C ++中,/都朝0取整,而不是-inf。

@克里斯,你说得对。我忘记了在C89中也是实现定义的。

它实际上生成无分支的代码。缺点是,当b <0时,它四舍五入为+ inf。

如果b为负,则产生不正确的结果。例如,-1 -2正在计算-1,而不是0。

@ Mr.Smith:我发布了一个也适用于负b的版本。

警告:此帖子产生错误的输入a = -1的结果。请参阅其他答案。 -凸轮

这可能就是您所谓的" kludgey",但这是我想出的;)

int divideDown(int a, int b){

int r=a/b;

if (r<0 && r*b!=a)

return r-1;

return r;

}

在if语句中,我输入r <0-但是我不确定这是否是您想要的。您可能希望将if语句更改为

if (a<0 && b>0)

这与您的描述"似乎就像我将负整数除以正整数一样"。

这就是我所说的克鲁格,但是我什么都不知道。特别是对于r * b!= a测试,它可能比a%b!= 0更有效,并且证明与提供的单行表达式相比,多余的代码行是合理的,我将此授予"答案"。

我对此不确定,但不应该是if (r<=0 && r*b!=a)吗?

它的后果更糟-如所写,这会为a = -1产生错误的答案,如果将其更改为r <= 0,那么对于a = 1会产生错误的答案。这个答案不应该被接受;它产生不正确的结果。

无法删除,因为它是可接受的答案。将尝试使用解决方案进行修复,但在此之前将进行警告编辑。感谢您发现错误!

@Cam我认为即使已经接受您也可以删除自己的答案。实际上,我认为这样做有一些自我批评的标志。也许不吧。

@csj:"您无法删除此接受的答案"

Math.Floor((double)a/(double)b)

如果您需要将其作为整数,请在之后进行转换

(int)Math.Floor((double)a/(double)b)

decimal是最慢的数字类型,在这里不添加任何内容。 float或double更合适。

您也不需要强制转换分母,只需转换分子。

@Matthew Flaschen:我正在考虑。我没有意识到他所说的int是int32。如果插入a和b的无关值[分别为Int64.MaxValue-1和Int64.MaxValue],则使用double会因为浮点错误而中断,并且您将得到1而不是0。答案已固定。

这个答案是可行的,但是鉴于fp的性能成本,我认为我的"笨拙"方法思想没有任何改善。

许多编程语言,包括较早的C和C ++标准,都保证了除法规则,即

a = b * (a / b) + a % b

即使a/b和a%b的确切值未定义。 (现在它们是用C和C ++定义的。)可以使用以下代码(等效于)以下代码来利用该语言在许多语言和平台中计算所需的结果:

int divF(int a, int b) { return a / b - (a % b < 0); }

这是@TomasPetricek的答案中的版本。但是,这仅适用于b > 0!

以下代码适用于任何b != 0:[1]

int sign(int x) { return (x > 0) - (x < 0); }

int divF2(int a, int b) { return a / b - (sign(a % b) == -sign(b)); }

但是,四舍五入的划分(又称地板划分,又称Knuth划分)并不总是可取的。有人认为[2]欧几里得除法是最普遍有用的除法。对于b > 0,它向下取整,对于b < 0,它向上取整。它具有一个很好的属性,即对于所有a,b,兼容定义的余数的值始终为非负值,而与它们的符号无关。此外,它与用于2的幂的除数的二进制补码机上的移位同时发生。是的,它的计算速度也更快:

int divE(int a, int b) {

int c = a % b < 0;

return a / b + (b < 0 ? c : -c);

}

所有这三个版本在amd64上使用clang 3.4.1 -O2生成无分支代码。但是,在二进制补码体系结构中,以下操作可能会更快一些:

int divE2(int a, int b) { return a / b + (-(a % b < 0) & (b < 0 ? 1 : -1)); }

生成的代码

divF:

cdq

idiv esi

shr edx, 31

sub eax, edx

divF2:

cdq

idiv esi

test edx, edx

setg cl

movzx ecx, cl

shr edx, 31

sub ecx, edx

test esi, esi

setg dl

movzx edx, dl

shr esi, 31

sub esi, edx

cmp ecx, esi

sete cl

movzx ecx, cl

sub eax, ecx

潜水:

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, dword ptr [rsi + rsi - 1]

and ecx, edx

add ecx, eax

mov eax, ecx // WTF? But OK when inlined...

基准测试

使用上述编译器,在i7-3720QM CPU @ 2.60GHz上,使用不同的TESTFUNC运行以下代码,可以得到

simple truncating division:

1225053229                 base:  8.41 ns

round to -inf for b > 0, broken for b < 0:

975205675                 divF:  8.70 ns

975205675               ben135:  8.71 ns

euclidean division:

1225082031                divE2:  9.37 ns

1225082031                 divE:  9.67 ns

round to -inf, work for b < 0:

975229215                Chazz:  9.67 ns

975229215                divF2: 10.56 ns

975229215           runevision: 13.85 ns

975229215        LegsDrivenCat: 16.62 ns

975229215                Warty: 20.00 ns

likely an overflow bug trying to implement the above:

975049659              DrAltan: 11.77 ns

第一列是产生相同结果的校验和分组算法。 base是用于测量测试开销的最简单的截断除法。

码:

#define STRINGIZE2(a) #a

#define STRINGIZE(a) STRINGIZE2(a)

int main()

{

srandom(6283185);

static const int N = 500000000;

int ret = 0;

for(int i = 0; i < N; ++i)

{

int a = random() - 0x40000000, b;

do b = (random() >> 16) - 0x4000; while(b == 0);

ret += TESTFUNC(a,b);

}

timespec t;

clock_gettime(CLOCK_VIRTUAL, &t);

printf("%10d %20s: %5.2f ns

", ret, STRINGIZE(TESTFUNC), (t.tv_sec*1.e9 + t.tv_nsec)/N);

}

参考文献

达安·雷恩计算机科学家分册和模数。 2001年12月。

雷蒙德·布特(Raymond T.函数div和mod的欧几里得定义。 1992年4月。

对于2的幂,取决于您的编译器实现(Visual Studio),可以使用右移。

-5 >> 2 == -2

我不是低级优化方面的专家,但是这是一个没有分支(条件)的版本,它也不需要转换为浮点数。经过C#测试,它似乎比使用分支的版本要快得多。

return (a - (((a % b) + b) % b)) / b;

此解决方案部分源自有关如何对负数执行最佳模函数的讨论:负数的模

上面的Atlans博士解决方案是最通用的答案,但是如果您知道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;将返回-1。 -2 * b + 1],但是如果您只是尝试确定除法的结果是否为正,那都没有关系。实际上,我们只是更改了舍入,以使其舍入为-1而不是0。

这是一个在除数为负数时可用的版本:

int 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;

}

我这样做(只有一个除法,没有乘法,没有模,看起来是最快的解决方案):

(a < 0 && b > 0) ? (a - b + 1) / b : (a > 0 && b < 0) ? (a - b - 1) / b : a / b

我很好奇什么更快:更少的分支或更少的乘法/除法/模,因此我将我的解决方案与runevision的解决方案进行了比较,在这种情况下,较少的划分优于较少的分支。

在某些情况下,ben135的解决方案给出错误的结果(例如10 / -3给出-3但应为-4)。

通过偏移2 ^ 31以无符号长进行计算,例如:

int floor_div(int a, int b)

{

unsigned long aa = a + (1UL << 31);

unsigned long cc = (aa / b) - (1UL << 31);

return (int)cc

}

我还没有测试。

编辑:我的解决方案并非在所有情况下都有效。这是Dr.Atlan解决方案的C#实现:

///

/// Divides a/b, rounding negative numbers towards -Inf.

///

/// Dividend

/// Divisor; must be positive for correct results

/// a ÷ b

public static int Div(int a, int b)

{

return a < 0 ? (a - b + 1) / b : a / b;

}

不能用于无余数可除的数字。例如,Div(-1, 1)返回-2。

@BrankoDimitrijevic:哈!那解释了我游戏中的工件。现在使用Dr.Altans解决方案;那个似乎运作良好。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值