如何正确避免 SIGFPE 和算术运算溢出

How to properly avoid SIGFPE and overflow on arithmetic operations

本文关键字:算术运算 溢出 SIGFPE 何正确      更新时间:2023-10-16

我一直在尝试创建一个尽可能完整的分数类,以自学C++,类和相关的东西。除此之外,我还想确保对浮点异常和溢出提供一定程度的"保护"。

目的:

避免常见运算中的算术运算中出现溢出和浮点异常,从而消耗最少的时间/内存。如果无法避免,那么至少要检测到它。

此外,这个想法是不要投射到更大的类型。这会产生一些问题(比如可能没有更大的类型)

我发现的案例:

  1. 溢出在 +, -, *,/, pow, 根

    操作大多很简单(abLong):

    • A+B:如果LONG_MAX - B> A,则存在溢出。(不够。ab可能是负面的)
    • A-B:如果LONG_MAX - A> -B,则存在溢出。(同上)
    • A*B:如果LONG_MAX/B>A,则存在溢出。(如果 b != 0)
    • a/b:如果 a <<b,可能会抛出 SIGFPE,如果 b <<0,则可能会溢出
    • pow(
    • a,b):如果 (pow(LONG_MAX, 1.0/b)> a,则存在溢出。
    • pow(a,1.0/b):类似于 a/b
  2. 当 x = LONG_MIN(或等效值)时在 abs(x) 上溢出

    这很有趣。每个有符号类型都有一个可能值的范围 [-x-1,x]。abs(-x-1) = x+1 = -x-1,因为溢出。这意味着存在 abs(x) <0

    的情况
  3. 大数除以 -1 的 SIGFPE

    应用分子/gcd(分子,分母)时发现。有时 gcd 返回 -1,我得到一个浮点异常。

简单修复:

  1. 在某些操作上很容易检查溢出。如果是这种情况,我总是可以转换为双倍(有失去大整数精度的风险)。这个想法是找到一个更好的解决方案,而不是铸造。

    在分数算术中,有时我可以对简化进行额外的检查:要求解 a/b * c/d(共素数),我可以先简化为共素数 a/d 和 c/b。

  2. 如果询问ab是 <0 还是> 0,我总是可以做级联。不是最漂亮的。除了这个糟糕的选择之外,我还可以创建一个函数 neg() 来避免溢出
    T neg(T x){if (x > 0) return -x; else return x;},
    
  3. 我可以接受 gcd 的 abs(x) 和任何类似情况(任何地方 x> LONG_MIN)

我不确定 2. 和 3. 是否是最好的解决方案,但似乎足够好。我在这里发布这些,所以也许有人有更好的答案。

最丑陋的修复

在大多数操作中,我需要执行大量额外的操作来检查并避免溢出。这是我很确定我可以学到一两件事。

例:

Fraction Fraction::operator+(Fraction f){
double lcm = max(den,f.den);
lcm /= gcd(den, f.den);
lcm *= min(den,f.den);
// a/c + b/d = [a*(lcm/d) + b*(lcm/c)] / lcm    //use to create normal fractions
// a/c + b/d = [a/lcm * (lcm/c)] + [b/lcm * (lcm/d)]    //use to create fractions through double
double p = (double)num;
p *= lcm / (double)den;
double q = (double)f.num;
q *= lcm / (double)f.den;
if(lcm >= LONG_MAX || (p + q) >= LONG_MAX || (p + q) <= LONG_MIN){
//cerr << "Aproximating " << num << "/" << den << " + " << f.num << "/" << f.den << endl;
p = (double)num / lcm;
p *= lcm / (double)den;
q = (double)f.num / lcm;
q *= lcm / (double)f.den;
return Fraction(p + q);
}
else
return normal(p + q, (long)lcm);
}  

避免这些算术运算溢出的最佳方法是什么?


编辑:这个网站中有一堆问题非常相似,但那些并不相同(检测而不是避免,未签名而不是签名,SIGFPE 在特定无相关情况下)。

检查所有这些,我发现了一些答案,修改后可能有用,可以给出一个合适的答案,例如:

  • 检测无符号添加中的溢出(不是我的情况,我正在使用有符号):
uint32_t x, y;
uint32_t value = x + y;
bool overflow = value < x; // Alternatively "value < y" should also work
  • 检测已签名操作中的溢出。这可能有点太笼统了,有很多分支,并且没有讨论如何避免溢出。

  • 答案中提到的CERT规则是一个很好的起点,但同样只讨论如何检测。

其他答案太笼统了,我想知道对于我正在查看的案例是否有更具体的答案。

您需要区分浮点运算和积分运算

关于后者,对unsigned类型的操作通常不会溢出,除了除以零,根据定义,这是未定义的行为 IIRC。这与 C(++) 标准要求对无符号数字进行二进制表示这一事实密切相关,这实际上使它们成为一个环。

相比之下,C(++)标准允许signed数字的多个实现(符号+幅度,1的补码或最广泛使用的2补码)。因此,签名溢出被定义为未定义的行为,可能是为了给编译器实现者更多的自由,为他们的目标机器生成高效的代码。这也是你担心abs()的原因:至少在2的补码表示中,没有一个正数在数量级上等于最大的负数。请参阅 CERT 规则进行详细说明。

在浮点端,SIGFPE历来是为了发出浮点异常信号而创造的。然而,鉴于当今处理器中算术单元的实现方式多种多样,SIGFPE应被视为报告算术错误的通用信号。例如,glibc参考手册列出了可能的原因,明确包括整数除以零。

值得注意的是,根据ANSI/IEEE Std 754(我想这是今天最常用的)的浮点运算是专门设计为一种防错的。这意味着,例如,当加法溢出时,它会给出无穷大的结果,并且通常会设置一个标志,您可以在以后检查。在进一步的计算中使用此无限值是完全合法的,因为浮点运算已为仿射算术定义。这曾经是为了允许长时间运行的计算(在慢速机器上)即使在中间溢出等情况下也能继续。请注意,即使在仿射算术中也禁止某些运算,例如将

无穷大除以无穷大或将无穷大减去无穷大。因此,底线是浮点计算通常不应导致浮点异常。然而,你可以有所谓的陷阱,每当上述标志被举起时,就会触发SIGFPE(或类似的机制)。