【问题标题】:Why does the snippet throw a Floating point exception?为什么代码片段会引发浮点异常?
【发布时间】:2020-10-01 04:58:59
【问题描述】:

这个

double what;
for (int i = 1; i < (long)pow(10, 7); i++)
    what = (i + i) / (i * i) - i; 

引发浮点异常(核心转储)。为什么? 我正在使用 clang++。

【问题讨论】:

  • @FrançoisAndrieux 不正确,除法 (i + i) / (i * i) 优先于减法。
  • @FrançoisAndrieux 除以零将是 UB,所以任何事情,包括浮点错误都是可能的。
  • 根据int 的大小,i * i 在接近pow(10, 7) 时可能会溢出
  • @dfri 哎呀。你是对的。
  • i * ii 接近pow(10, 7) 时溢出

标签: c++ loops exception arithmetic-expressions


【解决方案1】:

根据您的平台,int 可能是 32 位或 64 位宽。

来自[basic.fundamental]/2[basic.fundamental]/3 [摘录,强调我的]:

[basic.fundamental]/2

有五种标准有符号整数类型:“signed char”, “short int”、“int”、“long int”和“long long int”。在这个 列表,每种类型提供的存储空间至少与前面的相同 它在列表中。 [...] 普通 ints 有建议的自然尺寸 通过执行环境的架构;另一个签名 提供整数类型以满足特殊需求。

[basic.fundamental]/3

对于每个标准的有符号整数类型,都存在一个 对应的(但不同的)标准无符号整数类型: “unsigned char”、“unsigned short int”、“unsigned int”、 “unsigned long int”和“unsigned long long int”,每个 占用相同的存储量并具有相同的对齐方式 要求为对应的有符号整数类型; [...]

有符号和无符号整数类型应满足约束 在 C 标准第 5.2.4.2.1 节中给出

我们可以去C11 Standard draft [摘录,强调我的]:

5.2.4.2.1 整数类型的大小&lt;limits.h&gt;

[...] 它们的实现定义的值应等于或大于 幅度(绝对值)与所示的相同,符号相同。

[...]

  • int 类型对象的最大值:INT_MAX +32767

[...]

如果不了解目标/架构细节,这对我们没有帮助,但是,为了简化您的问题,让我们考虑使用固定宽度有符号整数的示例,并注意以下示例“很好”(在这种情况下):

#include <cstddef>
#include <math.h>

int main() {    
    double what;
    for (int32_t i = 1; i < (int32_t)pow(10, 4); i++)
    {
        what = (i + i) / (i * i) - i; 
    }
    (void)what;
    return 0;
}

而对于我尝试过的特定处决,以下结果会导致“浮点异常”(UB;龙可能会飞出我们的鼻子,见下文):

#include <cstddef>
#include <math.h>

int main() {    
    double what;
    for (int32_t i = 1; i < (int32_t)pow(10, 5); i++)
    {
        what = (i + i) / (i * i) - i; 
    }
    (void)what;
    return 0;
}

这里的关键是int32_t 的最大值是2,147,483,647,这意味着i * i 将溢出pow(10, 5) 大小的值。有符号整数溢出是未定义的行为 (UB),从那里开始任何事情都会发生。在这种情况下,可能是 UB,巧合,从表达式 i * i 的溢出中产生一个值 0,这反过来又导致表达式中除以零(再次是 UB) (i + i) / (i * i)更巧合,很可能是浮点异常的根源。

我在这里强调巧合,因为超出 UB 的任何点都会成为逻辑分析的无用目标;编译器供应商可能会假设我们从来没有 UB 并且一旦我们进入 UB 域就会做任何事情。我们看到的任何结果都应视为巧合,除非您在特定目标架构和硬件上使用 C++ 的某些非标准方言工作,例如有符号整数溢出等特殊情况可能被定义为非标准实现定义(因此由特定实现指定)而不是 UB。

【讨论】:

    【解决方案2】:

    for 循环中的i &lt; (long)pow(10, 7) 条件导致整数(i * i) 表达式变得大于其最大值,从而导致整数溢出和undefined behavior。在某些实现中,i 可能会变为 01,从而导致以下表达式: (i / i) 成为0。这反过来可能(在 Visual Studio 中),例如,在调试期间导致 Integer Division By Zero 异常。

    【讨论】:

      【解决方案3】:

      您的 int 溢出,因为 10^14 不适合我所见过的 int(请注意,int 取决于平台)。在某些平台上,10^7 已经溢出。有符号溢出总是会导致未定义的行为,有时会导致0

      • 您可能希望直接使用大小合适的类型(例如std::uint64_tdouble)来存储循环变量。
      • 您为每个循环迭代计算pow。在循环外计算一次。
      • 您的循环似乎抛弃了除最后一个计算之外的所有计算。也许你可以只计算那个?

      【讨论】:

      • pow 不是constexpr 应该不会影响任何事情。即使是constexpr 函数也可能在调试版本的每次迭代中重新计算;甚至一个非constexpr 函数也可能在循环之前只被调用一次作为优化。
      • @HolyBlackCat 据我了解 constexpr 函数,如果它们的参数都是 constexpr,则必须在编译时对其进行评估。这不应该依赖于调试版本的任何定义。
      • 仅从程序的行为来看,应该不可能确定它是否被评估过一次(以及它是否在编译时评估,如果你不使用结果constexpr 上下文)。据我了解,由于 as-if 规则,标准不能强制评估在编译时进行。
      • @HolyBlackCat 你是对的。我刚刚检查了标准。您是否认为任何编译器实际上会将调用推迟到运行时?
      • 我刚刚测试过,至少 GCC 似乎可以做到。示例:gcc.godbolt.org/z/C8w9MA 您可以通过在其中放置断点或查看程序集来检查函数是否被多次调用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多