gcc 优化跳过初始化分配的内存

gcc optimization skips initializing allocated memory

使用 gcc 4.9.2 20150304 64 位我遇到了这个明显奇怪的行为:

double doit() {
    double *ptr = (double *)malloc(sizeof(double));
    ptr[0] = 3.14;
    return (double)((uintptr_t) ptr);
}

在代码中,我在堆上分配一个 double,对其进行初始化,然后返回另一个 double,并使用第一个转换为 intptr_t 的地址进行初始化。这经过优化 -O2,在 32 位模式下生成以下汇编代码:

sub    [=11=]x28,%esp
push   [=11=]x8                   ;; 8 bytes requested
call   8048300 <malloc@plt>   ;; malloc 'em
movl   [=11=]x0,0x14(%esp)        ;; store zeros in upper 32bits
mov    %eax,0x10(%esp)        ;; store address in lower 32bits
fildll 0x10(%esp)             ;; convert a long long to double
add    [=11=]x2c,%esp
ret    

令人惊讶的是,分配的 double 的初始化完全消失了。

当使用 -O0 生成代码时,一切都按预期工作,相关代码是:

push   %ebp
mov    %esp,%ebp
sub    [=12=]x28,%esp
sub    [=12=]xc,%esp
push   [=12=]x8                    ;; 8 bytes requested
call   8048300 <malloc@plt>    ;; malloc 'em
add    [=12=]x10,%esp
mov    %eax,-0xc(%ebp)
mov    -0xc(%ebp),%eax
fldl   0x8048578               ;; load 3.14 constant
fstpl  (%eax)                  ;; store in allocated memory
mov    -0xc(%ebp),%eax
mov    %eax,-0x28(%ebp)        ;; store address in low 32 bits
movl   [=12=]x0,-0x24(%ebp)        ;; store 0 in high 32 bits
fildll -0x28(%ebp)             ;; convert the long-long to a double
fstpl  -0x20(%ebp)
fldl   -0x20(%ebp)
leave  
ret    

问题

我是否做了任何无效的事情(我特别考虑别名规则,即使在我看来跳过初始化没有正当理由)或者这只是一个 gcc 错误?

请注意,在编译为 64 位代码时会出现同样的问题(正式的 intptr_t 在 64 位模式下是 8 个字节,因此 ad double 无法准确表示它...然而,这不会发生,因为在 x86-64 上,仅使用 64 位地址中的 48 位,并且 double 可以准确表示所有这些值)。

允许优化在UB的情况下删除代码,但这里不应该。

您在 Value *ptr = (Value *)malloc(sizeof(Value)); 中进行了不必要的转换,但这应该是无害的。

这一行res.d = (unsigned long long) ptr;最好是res.d = (intptr_t) ptr;,因为intptr_t明确允许接收指针,然后你可以在double变量中设置一个整数值: 你可以失去精度,但它不应该是 UB。

我无法测试它(因为我没有 gcc 4.9)但是如果你有同样的问题:

#include <stdint.h>

...

Value doit() {
    Value *ptr = malloc(sizeof(Value));
    ptr[0].u = 7;
    Value res; res.d = (double) ((intptr_t) ptr);
    return res;
}

我会得出一个 gcc 错误的结论。

我可以尝试在 FreeBSD 10.1 上使用 clang 版本 3.4.1 编译 简化 版本的代码

cc -O3 -S doit.c 给出(精简到代码部分):

doit:                                   # @doit
# BB#0:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-8, %esp
    subl    , %esp
    movl    , (%esp)
    calll   malloc
    movl    74339512, 4(%eax)    # imm = 0x40091EB8
    movl    74389535, (%eax)     # imm = 0x51EB851F
    movl    %eax, 8(%esp)
    movl    [=11=], 12(%esp)
    fildll  8(%esp)
    movl    %ebp, %esp
    popl    %ebp
    ret

它与 gcc 的编译不同,但 clang 甚至在 -O3 优化级别进行 3.14 初始化(3.14 的转储十六进制是 0x40091eb851eb851f


阅读其他评论和答案后,我认为问题的真正原因是 gcc 跳过 中间转换并将 return (double)((uintptr_t) ptr); 读取为 return (double) ptr; - 不完全是因为它会是一个语法错误,但仍然认为存在 UB,因为 最后 一个指针值结束为一个双精度变量。但是如果我们用中间转换 分解行 它应该被读作(恕我直言)为 :

register intptr_t intermediate = (intptr_t) ptr; // valid conversion
return (double) intermediate;  // valid conversion

这似乎是一个错误...即使使用简化代码

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

double doit() {
    double *ptr = (double *)malloc(sizeof(double));
    ptr[0] = 3.14;
    uintptr_t ip = (uintptr_t)ptr;
    return (double)ip;
}

int main(int argc, const char *argv[]) {
    double v = doit();
    double *p = (double *)((intptr_t)v);
    printf("sizeof(uintptr_t) = %i\n", (int)sizeof(uintptr_t));
    printf("*p = %0.3f\n", *p);
    return 0;
}

使用 -O2 编译时不会初始化内存。

代码工作正常,直接返回 intptr_t(或 unsigned long long);但是在转换为 double 后返回它不起作用,因为 gcc 显然假设在这种情况下您将无法再访问内存。

这在 32 位模式下显然是错误的(其中 intptr_t 是 4 个字节,double 为整数提供 53 位精度)但对于 64 位模式也是如此 while uintptr_t 确实是 8 个字节,使用的值是 48 位)。

编辑

对此不确定,但问题可能与 "dead code elimination on tree" (-ftree-dce) 有关。在 32 位模式下编译时启用优化 -O2 但禁用此特定的 -fno-tree-dce 程序输出更改并且是正确的但生成的代码 不是 .

更具体地说,doit 的非内联版本不包含初始化代码,但 main 中生成的代码内联调用和优化器 "knows",内存的值为3.14 并直接在输出中打印出来。

编辑 2

已确认为错误,已在主干中更正。

下一个版本之前的解决方法是 -fno-tree-pta

我看不出有什么奇怪的。你从来没有读过你写的 7 ,而是将 malloc 的结果写到 double:

Value *ptr = (Value*) malloc(sizeof(Value));
ptr[0].u = 7;
Value res; res.d = (uintptr_t) ptr; // ptr is a result of malloc
return res;  // ptr is lost here which probably makes 
             // GCC think that it is no longer accessible
             // so "7" is lost here too

并且将指针转换为双精度数很可能会失去精度,从而使内存无法访问 (UB)。

但是,如果您将指针保存为整数 (.u),GCC 会将其视为别名内存并保持初始化:

Value res; res.u = (uintptr_t) ptr; // Saving to .u, not .d

编译为

0x0000000000400570 <+0>:     sub    [=12=]x8,%rsp
0x0000000000400574 <+4>:     mov    [=12=]x8,%edi
0x0000000000400579 <+9>:     callq  0x400460 <malloc@plt>
0x000000000040057e <+14>:    movq   [=12=]x7,(%rax)
0x0000000000400585 <+21>:    add    [=12=]x8,%rsp
0x0000000000400589 <+25>:    retq   

所以问题是你将指针保存为双精度。


顺便说一句,(double)ptr 是一个编译错误,按照标准要求:

6.5.4 Cast operators

[...]

4 A pointer type shall not be converted to any floating type. A floating type shall not be converted to any pointer type.

截至 N1548 草案

C 不是汇编程序。 C 可以调用未定义的行为,而将其视为高级汇编程序的人看不出原因。例如:给定两个数组 int a [10] 和 int b [10],巧合的是 &a [10] == &b [0] 是可能的。但是,下面的代码

int a [10],  b [10];
int* p = &a [10];
if (p == &b [0]) *p = 0;

如果 p == &b [0],则调用未定义的行为。两个指针 p 和 &b [0] 比较相等,并且由相同的位组成,但行为不同。 (如果您不同意,请查看 "restrict" 指针,其中的重点是比较相等的指针可以有不同的行为)。

转换为uintptr_t的规则如下:每个有效的指针都可以转换为uintptr_t,结果可以转换回指针,给出相同的指针。这些值是实现定义的,除了将空指针转换为 uintptr_t 必须给出零,将 0 转换为指针必须给出空指针。没有什么要求转换应该很简单,或者应该是你认为应该的那样。

到 uintptr_t 的转换是实现定义的。如果体系结构中的指针被限制为 n <= 62 位,那么转换完全有可能如下所示:如果 p 是空指针,则将其转换为零。如果 p 不是空指针,则取 n 位,将它们向左移动 (63 - n) 位,或结果为 0x8000 0000 0000 0001。结果保证不会无损地转换为 double。当 uintptr_t 转换为 double 时,结果是无法再将其转换回有效指针。

因此,如果 (double)(uintptr_t)p 是从 p 派生的唯一值,则 p 无法重构,指针 p 丢失,可以优化对 *p 的赋值离开,因为 *p 无法再次读取。

我相信 gcc 是正确的。您没有使用该值,也没有 return 指向它的指针,因此它认为该值已变得无法访问。

您应该 return 编辑指针并在其他地方转换为 double,或者您需要一个联合以便 gcc 知道指向该值的指针仍然存在:

union CrazyDouble {
   double v;
   double *p;
};