为什么这个循环没有被优化掉?

7

我有一个非常简单的C语言程序,它将数组A中的所有元素复制到数组A中。例如:

double *A;
A = (double*)malloc(sizeof(double)*SIZE);
for( i = 0; i < SIZE; i++) {
  A[i] = A[i];
}

我原本期待编译器会优化掉这段代码并最终变成一个noop(无操作)。然而,通过测量此循环的运行时间并查看汇编代码,似乎确实将元素从内存加载到寄存器中,然后存回相同的内存位置。我已启用-O3。有谁能解释一下为什么C语言编译器不进行优化呢?或者我错过了什么重要的信息吗?
非常感谢。

3
在x86_64架构下,使用GCC 4.5无法重现此问题。在使用-O3选项编译时,汇编中不会生成那个循环,只有malloc调用被保留了。提供此信息仅供参考。 - Mat
2
你得到了哪个汇编文件?对我来说,在GCC 4.5.0中它被优化了。 - Steve Blackwell
7个回答

7

我的gcc(版本4.6.1)已将其优化掉。

$ cat 7680489.c
#include <stdlib.h>
#define SIZE 100
int main(void) { double *a; size_t i;
// 使用calloc函数初始化元素 a = calloc(SIZE, sizeof *a); for (i = 0; i < SIZE; i++) a[i] = a[i]; free(a); // 释放内存空间
return 0; }
$ gcc -std=c89 -O3 -S 7680489.c
$ cat 7680489.s
        .file   "7680489.c"
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB3:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $8, %esi
        movl    $100, %edi
        call    calloc
        movq    %rax, %rdi
        call    free
        xorl    %eax, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE3:
        .size   main, .-main
        .ident  "GCC: (Debian 4.6.1-4) 4.6.1"
        .section        .note.GNU-stack,"",@progbits

这段代码没有循环。当使用malloc而不是calloc时,汇编输出非常相似。我切换到calloc,以避免对象具有不确定的值(感谢R..)。


7

从硬件角度来看,加载和保存双精度浮点数并不是没有操作;如果它是IEEE双精度浮点数的几个陷阱表示之一,其位值可能会发生变化。

例如,如果您将NaN加载到寄存器中,则会将其写出为规范NaN值,其位值可能不相同。


谢谢回复。然而,这也会发生在整数数组中。 - user983027
作为一个附注,对于某些体系结构,它是无操作的。例如,PowerPC 601。 - Dietrich Epp
如果假设FENV_ACCESS关闭,为什么C编译器会关心呢? - Cubbi
gcc 忽略 FENV_ACCESS 告示,因此它需要表现得好像它总是开启的。不幸的是,它只能部分正确地表现... - R.. GitHub STOP HELPING ICE

2
为了优化循环,编译器需要识别以下几点:
  • 加载/存储不会改变数据(例如由于浮点NaN转换)
  • 两个数组地址相同
  • 两个数组索引表达式相同
  • 考虑地址和索引后,加载和存储不会重叠,而是完全重合。
  • 存储不会“覆盖”其他存储的结果或尚未加载的值。
  • 加载/存储不能导致存储故障。这反过来要求编译器认识到存储来自malloc,并且循环不会超出分配的末尾。
  • 循环将在有限次迭代后终止
  • 还有可能有其他几点我没有想到的
请记住,优化的目标是消除“正常”的冗余,而不是消除“课堂例子”。

编译器不必识别或预测存储故障。数组超出索引会导致未定义的行为。这可能意味着段错误,也可能不是。 - psusi
取决于编译器以及它所遵循的“接触”方式。 - Hot Licks
你说的“contact”是什么意思?标准规定行为未定义,因此segv或无segv都是有效结果。由于两种结果被视为相同,所以优化和不优化之间没有区别,因此允许进行优化。 - psusi
我拼错了,应该是“合同”(contract)。 - Hot Licks

1

编译器并不会有实际的思考。
它只能优化匹配预设模式的内容。

也就是说,如果代码不符合已经在编译器中预编程的已知无操作(no-op)模式,则不会被消除。

通过输入 A[i] = A[i] ,您轻微地更改了模式,使其不再匹配 empty loop pattern


根据 OP 的说法,整数数组也会发生同样的事情,那么这怎么是一个错误的答案呢? - Johan
实际上,编译器主要通过进行许多小的优化来进行优化,而不是在高层次上识别“模式”。例如,编译器会将两个数组的数组索引合并,因为它们具有相同的指针和数组索引。然后它会认识到存在一个值的副本。 (但是,正如我在帖子中所指出的那样,在消除复制之前仍然存在许多障碍。) - Hot Licks

0
这里的问题在于您正在使用指针。 由于编译器无法假设指针只能读/写内存中的任何位置,因此很难优化指针。 改用[]数组运算符并重试。您应该会看到期望的优化。

0
作为一般性的信息,编译器最难优化的两个方面是循环和指针,而你的例子涉及到了这两个方面。编译器知道在循环中值经常会改变,因此在优化时非常保守。另外,A是一个指针,编译器知道指针可能会因各种因素而改变,因此在修改指针时也会退缩。这就是为什么编译器在处理你的例子时会遇到困难的原因。

编译器对循环有问题?真的吗?我相信指针会有问题,但我很想为循环找到一个引用。 - Johan
我没有写得很清楚。编译器很难知道一个值在循环中是否会改变。 - dbeer

0
这段代码存在未定义的行为(使用具有不确定值的对象),那么您为什么会对它执行的操作有任何期望呢?

实际上,在优化中最令人担忧的事情之一就是未初始化的值。许多编译器在值未初始化时会放弃处理,这主要是因为它们严重依赖于值传播算法。 - Hot Licks
为什么这是未定义行为?在这种情况下,他分配了一些内存,取出未初始化的值并将它们保存回去。这里没有发生任何不好的事情。 - tangrs
任何对具有不确定值的对象的使用都是未定义行为。 - R.. GitHub STOP HELPING ICE

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