有符号数除以无符号数的问题

7

我正在尝试计算滚动平均值,并为了尝试并优化一下,我简化了计算,只有一个除法。当值减少时,存在一个点,当前值降低到小于平均值。在这一点上,平均值会跳跃。我想这是因为除法是无符号的,我的分子的符号位被解释为一个巨大的无符号数。我不确定需要在哪里进行无符号转换才能确保此问题不再出现。

unsigned int AverageUsage;
unsigned int TotalUsage;
unsigned int incCount;

    AverageUsage = (TotalUsage - AverageUsage)/++incCount + AverageUsage;

AverageUsage始终为正数,但当TotalUsage低于AverageUsage时,对于除法运算我不确定会发生什么

    AverageUsage = (signed int)(TotalUsage - AverageUsage)/++incCount + AverageUsage;

将分子设置为有符号数,但我不确定如何进行除法运算。

    AverageUsage =  (signed int)((signed int)(TotalUsage - AverageUsage)/++incCount) + AverageUsage;

这应该是可行的(我可以保证整个操作的结果永远不会是负数),但我担心当incCount达到一个“看起来”为负数的值时会出现问题。

有没有一个简单的解决方案,希望它:

  • 不需要if语句
  • 不需要QWORDs

谢谢!


3
请提供所有变量的声明,这对于确定C语言中各个子表达式类型的提升规则非常重要。例如,AverageUsage是int类型、unsigned int类型、还是unsigned short类型等。请注意,C语言的类型提升规则会根据不同子表达式的类型而有所不同。 - Nemo
我对这段代码持怀疑态度;你确定它在算术上是正确的,并且计算的是“滚动平均值”,而不是“累积平均值”吗?滚动平均值需要一个“最近值”的缓冲区。 - Clifford
@Clifford。它是一个基本的IIR。您可能在想积分器-组合FIR;其等效于统计样本平均值(运行/滚动)。无论如何,它们都是正确的,作为低通滤波器和种群平均值的近似。 - Tyson Hilmer
5个回答

5
C二进制运算(包括除法)的一般规则是,操作数将被转换为相同类型之一,这些类型包括:int、unsigned int、long、unsigned long、intmax_t、uintmax_t、float、double、long double。如果两个操作数都属于该列表中的类型,则它们都将被转换为后面的那个类型。如果两者都不属于该列表,则它们都将被转换为int。
因此,在您的示例中:
AverageUsage = (signed int)(TotalUsage - AverageUsage)/++incCount + AverageUsage

如果 incCountunsigned int,那么你的强制类型转换没有效果,减法将被转换为有符号整数,然后再转回无符号整数进行无符号除法。如果你想要有符号除法,你需要使用以下代码:
AverageUsage = (int)(TotalUsage - AverageUsage)/(int)++incCount + AverageUsage

正如您所指出的那样,如果incCount超过INT_MAX,可能会导致问题。

通常,处理器除法指令只指定一种类型,该类型用于两个操作数。当存在用于不同类型的除法的特殊指令时,通常是针对较大(双宽度)的被除数,而不是不同的符号。


4

你有两个选择。

使用浮点数运算

我认为你想这样做是为了得到一个正确的平均值。

不存在混合浮点/整数除法。因此,分子和分母都将转换为浮点数。

无论分子或分母是有符号还是无符号都没有关系。不存在无符号浮点数。分母incCount将被转换为浮点数,并进行完全的浮点数除法。

使用整数除法并处理特殊情况

如果出于某种原因您想保持整数除法,则分子和分母必须是相同的有符号/无符号类型。

分子和分母都是有符号数

incCount将被转换为有符号数。如果它太大,那么它看起来像一个负数,你的答案就会错误。您必须测试此溢出。

分子和分母都是无符号数

您必须使分子无符号,并使用if ()语句来处理两种情况:TotalUsage < AverageUsageTotalUsage > AverageUsage。这里incCount可以使用整数位的全部范围,因为它将被视为无符号数字。


好的,我明白了。我确实想要整数除法,因为我正在跟踪内存使用情况(以字节为单位),这几乎总是在50MB以上。字节的小数部分不用担心。我还在使用没有FPU的ARM进行开发。 - Gdogg

1
请注意,这不是标准平均值。标准平均值应为:
Averageusage = TotalUsage / ++incCount

假设(理想情况下)incCount是一些有用的定期增加的值(例如秒)。

衰减平均通常更像实现方式是:http://donlehmanjr.com/Science/03%20Decay%20Ave/032.htm 如果我翻译正确的话,是:

AverageUsage = TotalUsage / (incCount+1) + incCount/(incCount+1) * AverageUsage;
incCount++;

如Himadri所提到的,这些可能应该使用浮点运算来完成。

我正在尝试将所需除法的数量最小化。我的公式是您的简化版。 - Gdogg
@Gdogg:除非你有一些实验证据表明这是一个热点,否则我强烈建议你不要过早优化。使用正确、标准的算法会让你的用户更加满意,因为它能够正确地反映人们在看到平均值时的期望。 - Seth Robertson
简化不仅仅是关于性能的。在实践中,你对它的表达方式会出现严重问题;在整数算术中,(incCount/(incCount+1))总是为零。如果你重新排列成(incCount*AverageUsage)/(incCount+1),你就有可能在分子上溢出。 - Brooks Moses
@Brooks Moses:我认为您会发现我建议使用浮点算术。 - Seth Robertson
2
我认为TotalUsage在这里表示当前样本,如果是这样的话,@Gdogg的版本是标准平均值,你的版本也是(衰减平均值会在incCount的位置使用固定值—请参见您链接到的文章的B部分)。但是,你的版本必须使用浮点数——而不是“可能”!即使如此,@Gdogg的版本更加数值稳定,正确并且足够标准,可以在Knuth的“计算机编程艺术”第2卷(第3版的p232上的eq.(15),该方法归功于Welford(1962年))中找到。 - Matthew Slattery

0
你真的需要一个滚动平均,还是可以使用其他低通滤波器?单极点(有时称为“alpha”)滤波器可能适合你:
new_output = alpha * previous_output + (1-alpha)*new_input;
previous_output = new_output;

其中alpha介于0和0.9999之间。

alpha越接近1,滤波器就越“慢”。

您可以使用浮点数进行简便操作,或者使用整数进行直接操作。


0

如果TotalUsage < AverageUsage是可以预见且有效的,那么这些变量完全不适合使用无符号类型。 TotalUsage < AverageUsage将意味着AverageUsage可能为负数(如果TotalUsage < AverageUsage,则会出现这种结果)。如果被“平均”的数据从未为负数,则TotalUsage < AverageUsage为真是算术上不可能的。

如果TotalUsage < AverageUsage无效,则其为真将表明您的代码存在错误或算术溢出。您可以通过assert来防范这种可能性;也许实现为在发布版本中删除的宏。如果assert发生,则输入数据无效,或者发生了溢出,在后一种情况下,数据类型太小,应该使用long long、unsigned long long或double。

即使进行强制转换,如果TotalUsage < AverageUsage为真,则表达式的结果在算术上为负数,但最终分配给无符号类型,因此结果仍然不正确。

最终结论是,TotalUsage < AverageUsage 永远不可能为真,或者你的数据类型不适当。解决方案几乎肯定不是任何类型转换。

我的建议通常是对进行算术运算的变量始终使用有符号类型。这是因为混合有符号/无符号算术的语言语义有些晦涩难懂,并且中间操作可能会生成负值。即使对该变量的负值在语义上没有意义,我仍然建议在所有情况下使用有符号类型,只要该类型的正值范围仍足以避免溢出,并且在范围不足以使用更大的类型时,而不是使用相同大小的无符号类型。此外,在需要对无符号类型进行算术运算时,那么所有操作数都应为无符号(包括文字),并且中间操作不应结果下溢或上溢。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接