如何正确避免 SIGFPE 和算术运算溢出
How to properly avoid SIGFPE and overflow on arithmetic operations
我一直在尝试创建一个尽可能完整的分数类,以自学C++,类和相关的东西。除此之外,我还想确保对浮点异常和溢出提供一定程度的"保护"。
目的:
避免常见运算中的算术运算中出现溢出和浮点异常,从而消耗最少的时间/内存。如果无法避免,那么至少要检测到它。
此外,这个想法是不要投射到更大的类型。这会产生一些问题(比如可能没有更大的类型)
我发现的案例:
-
溢出在 +, -, *,/, pow, 根
操作大多很简单(
a
和b
Long
):- A+B:如果LONG_MAX - B> A,则存在溢出。(不够。
a
或b
可能是负面的) - 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
- A+B:如果LONG_MAX - B> A,则存在溢出。(不够。
-
当 x = LONG_MIN(或等效值)时在 abs(x) 上溢出
这很有趣。每个有符号类型都有一个可能值的范围 [-x-1,x]。abs(-x-1) = x+1 = -x-1,因为溢出。这意味着存在 abs(x) <0
的情况 - 大数除以 -1 的 SIGFPE
应用分子/gcd(分子,分母)时发现。有时 gcd 返回 -1,我得到一个浮点异常。
简单修复:
- 在某些操作上很容易检查溢出。如果是这种情况,我总是可以转换为双倍(有失去大整数精度的风险)。这个想法是找到一个更好的解决方案,而不是铸造。
在分数算术中,有时我可以对简化进行额外的检查:要求解 a/b * c/d(共素数),我可以先简化为共素数 a/d 和 c/b。
- 我可以接受 gcd 的 abs(x) 和任何类似情况(任何地方 x> LONG_MIN)
a
或b
是 <0 还是> 0,我总是可以做级联。不是最漂亮的。除了这个糟糕的选择之外,T neg(T x){if (x > 0) return -x; else return x;},
我不确定 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
(或类似的机制)。
- 算术运算的结果类似于:C浮点变量中的1/3
- 更快的C++算术运算
- 为什么循环体中的一个基本算术运算执行得比两个算术运算慢
- 为什么对无符号字符进行算术运算会将它们提升为有符号整数
- 如何在 C++ 中计算字符串的算术运算
- 复数的算术运算
- 具有非常非常大数字的算术运算
- 如何使用C++中的处理器指令来实现快速算术运算
- boost::spirit::x3 phrase_parse 在进入 Vector 之前进行算术运算
- C++中混合数据类型的算术运算
- C++:编译器是否优化整数 + 浮点算术运算?
- 如何在特征中对二维数组的列执行简单的算术运算
- 使用数字列表和算术运算获取目标数字
- C++:“enable_if”用于限制支持特定算术运算的类型
- 如何正确避免 SIGFPE 和算术运算溢出
- 编译器用于编译 128 位整数的基本算术运算的技巧
- 将临时值存储为某种数据类型时,算术运算的标准规则是什么
- 相同的算术运算在 C++ 和 Python 中给出不同的结果
- 检查算术运算中是否存在溢出情况
- 当算术运算溢出时,它们会产生一个确定性的数字