【问题标题】:behavior when for-loop variable overflow and compiler optimizationfor循环变量溢出和编译器优化时的行为
【发布时间】:2021-05-22 19:30:09
【问题描述】:

在阅读 http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html 关于 c 中未定义行为的信息时,我收到了有关此示例的问题。

for (i = 0; i <= N; ++i) { ... }

在这个循环中,编译器可以假设循环会迭代 如果“i”在溢出时未定义,则恰好 N+1 次,这允许 广泛的循环优化可以启动。另一方面,如果 该变量被定义为溢出时环绕,然后编译器 必须假设循环可能是无限的(如果 N 是 INT_MAX) - 然后禁用这些重要的循环优化。 这尤其会影响 64 位平台,因为有这么多代码使用 "int" 作为归纳变量。

这个例子是为了展示 C 编译器可以利用未定义的行为来假设执行时间是精确的 N+1。但我不明白为什么这个假设是有效的。

我可以理解,如果变量被定义为在溢出时环绕并且 N 是 INT_MAX,那么 for 循环将是无限的,因为我将从 0 转到 INT_MAX 并溢出到 INT_MIN,然后循环到 INT_MAX 并从 INT_MIN 重新开始等等。因此编译器无法对执行时间做出这种假设,也无法在这一点上进行优化。

但是如果 i 在溢出时未定义呢?在这种情况下,i 通常从 0 循环到 INT_MAX,然后 i 将被分配 INT_MAX+1,这将溢出到未定义的值,例如 0 和 INT_MAX 之间。如果是这样,条件i&lt;= INT_MAX仍然有效,for循环是否应该不继续并且也是无限的?

【问题讨论】:

  • 那么请发音合理。
  • 未定义的行为是未定义的。执行具有未定义行为的程序的结果是在您的浴室中产生了一头大象。
  • @EOF:这个问题有充分的理由,唯一的错误是在第三句中,溢出int 将产生一些int 值,而不是被允许产生任何行为。
  • 没有 INT_MAX+1 就像没有 ∞+1。它从 (int) -MAX 开始。 os.mbed.com/handbook/C-Data-Types
  • @wired:OP 表示INT_MAX+1,即比INT_MAX 大一的数学整数,它确实存在(在任何抽象数存在的意义上)。观察 C 标准本身使用类似的语言; + 被定义为产生“操作数之和”,即使存在溢出时不存在这样的和。说明溢出会发生什么的措辞是分开的,即使在同一个子句中也是如此。 OP 在讨论 i++ 如何假设 INT_MAX 和 1 的总和时做了同样的事情,并分别讨论了在出现这种溢出时会发生什么。

标签: c


【解决方案1】:

…然后我将被分配INT_MAX+1,这将溢出到一个未定义的值,例如 0 和 INT_MAX 之间。

不,这是不正确的。写得好像规则是:

  • 如果++i 溢出,那么i 将被赋予一些int 值,尽管没有指定是哪一个。

但是,规则是:

  • 如果++i 溢出,则程序的整个行为未由 C 标准定义。

也就是说,如果++i 溢出,C 标准允许这些事情发生:

  • i 留在INT_MAX
  • i 更改为 INT_MIN
  • i 变为零。
  • i 更改为 37。
  • 处理器生成陷阱,操作系统终止您的进程。
  • 其他一些变量会改变值。
  • 程序控制跳出循环,就像正常结束一样。
  • 任何东西。

现在考虑编译器在优化中使用的这个假设:

...编译器可以假设循环将精确地迭代 N+1 次...

如果++i 只能将i 设置为某个int 值,那么正如您得出的结论,循环不会终止。另一方面,如果编译器生成的代码假定循环将精确地迭代 N+1 次,那么在++i 溢出的情况下会发生其他情况。究竟会发生什么取决于循环的内容以及编译器对它们的处理方式。但这并不重要:C 标准允许生成此代码,因为当 C 标准允许 ++i 溢出时发生whatever

【讨论】:

    【解决方案2】:

    让我们考虑一个实际案例:

    #include <limits.h>
    #include <stdio.h>
    
    unsigned long long test_int(unsigned long long L, int N) {
        for (int i = 0; i <= N; ++i) {
            L++;
        return L;
    }
    
    unsigned long long test_unsigned(unsigned long long L, unsigned N) {
        for (unsigned i = 0; i <= N; ++i) {
            L++;
        return L;
    }
    
    int main() {
        fprintf(stderr, "int: %llu\n", test_int(0, INT_MAX));
        fprintf(stderr, "unsigned: %llu\n", test_unsigned(0, UINT_MAX));
        return 0;
    }
    

    博客文章的重点是上述代码的编译器可能的行为:

    • 对于test_int(),编译器可以确定,对于从INT_MIN-1 的参数值,函数应该返回不变的L,对于0INT_MAX-1 之间的值,返回值应该是@ 987654329@ 和 INT_MAX 的行为未定义,因此返回 L + N + 1 也可以,因此代码可以简化为

        unsigned long long test_int(unsigned long long L, int N) {
            if (N >= 0)
                L += N + 1;
            return L;
        }
      
    • 对于test_unsigned(),同样的分析得出:对于低于UINT_MAX 的参数值,返回值为L + N + 1,对于UINT_MAX,存在无限循环:

        unsigned long long test_unsigned(unsigned long long L, unsigned N) {
            if (N != UINT_MAX)
                return L + N + 1;
            for (;;);
        }
      

    https://godbolt.org/z/abafdE8P4 可以看出,gcc 和 clang 都为test_int 执行此优化,利用溢出时未定义的行为但为test_unsigned 生成迭代代码。

    【讨论】:

      【解决方案3】:

      有符号整数溢出调用未定义行为。程序员不能假设可移植程序会以特定方式运行。

      另一方面,使用特定版本的编译器并使用相同版本的库为特定平台编译的程序将表现确定性。但是您不知道这些行为是否会保持不变(即编译器、编译器版本等)。

      因此,您的假设对于特定的构建和执行环境可能是有效的,但通常是无效的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-01-27
        • 2021-02-17
        • 1970-01-01
        • 2012-12-11
        • 1970-01-01
        • 2013-08-15
        • 2017-09-01
        相关资源
        最近更新 更多