是否允许编译器删除结构成员的赋值,如果它没有明显使用?

时间:2013-01-25 15:56:49

标签: c standards-compliance

请考虑以下代码:

char buffer[256];

struct stX
{
    int a;
    int b;
    int c;
};

void * my_memcpy ( void * destination, const void * source, size_t num );

int main()
{
    struct stX x;
    x.a = 1;
    x.b = 2;
    x.c = 3;
    my_memcpy(buffer, &x.b, 2 * sizeof(int));
    {
        int i;
        for (i = 0; i < 2 * sizeof(int); ++i) {
            printf("%d\n", buffer[i]);
        }
    }
    return 0;
}

嵌入式系统的特定编译器决定删除对x.a和x.c的赋值(因为它们从未使用过(至少不是很明显))。这是c标准允许的优化吗?

当然,在包含的赋值中将结构实例定义为volatile导致。

gcc和msvc不执行此优化(但这不是真正的推理)。

更新:正如某些答案(正确)假设的那样,编译器可能会因为已知的memcpy定义而进行优化,但是,这不是我的特定实现所做的。 (它保留了堆栈结构的内存,只是不执行赋值。)假设我们用一个编译器没有定义的函数替换了memcpy。另外,假设在函数调用之后使用缓冲区。我相应地更新了上面的示例代码。

4 个答案:

答案 0 :(得分:3)

是的,只要应用程序的可观察行为没有改变,编译器就可以自由地做任何事情。 *

但是这假设您有一个符合的程序,即具有明确定义的行为的程序。您的代码没有表现出明确定义的行为;通过取消引用指向x.c的指针来访问x.b无效(这是您隐含地要求memcpy做的事情)。

更新:上述段落可能不正确;请参阅评论中的讨论......

<小时/> <子> * 有关更严格的定义,请参阅C99标准的5.1.2.3节:

  

访问易失性对象,修改对象,修改文件或调用函数   任何这些操作都是副作用,这是状态的变化   执行环境。

     

...

     

这是   实际实现不需要评估表达式的一部分,如果它可以推导出它   不使用该值,并且不产生所需的副作用   

答案 1 :(得分:0)

GCC执行完全相同的优化(使用x86_64和-O3):

        .file   "test.c"
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB12:
        .cfi_startproc
        movl    $2, -20(%rsp)
        movl    $3, -16(%rsp)
        movq    -20(%rsp), %rax
        movq    %rax, buffer(%rip)
        ret
        .cfi_endproc
.LFE12:
        .size   main, .-main
        .comm   buffer,256,32
        .ident  "GCC: (GNU) 4.7.2 20120921 (Red Hat 4.7.2-2)"
        .section        .note.GNU-stack,"",@progbits

正如人们所读,x.a = 1未被执行。它可能会进一步优化。

答案 2 :(得分:0)

。编译器可以对代码应用任何优化,假设变量值不能“自行”更改。

来自wikipedia

void bar(void) {
    foo = 0;

    while (foo != 255)
         ;
}

优化编译器会注意到没有其他代码可以更改存储在foo中的值,并且会假设它始终保持等于0。因此,编译器将使用类似于此的无限循环替换函数体:

void bar(void) {
    foo = 0;

    while (true)
         ;
}

但是,foo可能表示可以随时由计算机系统的其他元素更改的位置,例如连接到CPU的设备的硬件寄存器。上面的代码永远不会检测到这样的改变

为防止编译器优化上述代码,请使用 volatile 关键字:

static volatile int foo;

void bar (void) {
    foo = 0;

    while (foo != 255)
        ;
}

答案 3 :(得分:0)

一些初步观点:

  • 您的main函数返回void,这意味着就标准而言,您的程序行为未定义。但我们假设您的实现明确允许voidmain返回。此外,memcpy的范围没有原型,因为您没有再次包含string.h UB。

  • buffer中的值从未使用过,因此实际上整个程序没有可观察到的行为。因此,允许编译器删除代码的任何或所有。就标准而言,一个什么都不做的程序和另一个程序一样好。

我会假装您编写了一个程序,在2*sizeof(int)之后从buffer打印memcpy字节:

#include <string.h>
#include <stdio.h>

char buffer[256];

struct stX
{
    int a;
    int b;
    int c;
};

int main()
{
    struct stX x;
    x.a = 1;
    x.b = 2;
    x.c = 3;
    memcpy(buffer, &x.b, 2 * sizeof(int));
    {
        int i;
        for (i = 0; i < 2 * sizeof(int); ++i) {
            printf("%d\n", buffer[i]);
        }
    }
    return 0; /* just in case this is C89 */
}

因此,据我所知,编译器在此代码中删除x.a的赋值是正常的,但删除x.c的赋值意味着您的编译器无法符合标准。除非结构包含奇怪的填充。

如果 sizeof(int)中的bc之间至少有struct stX个字节的填充,则删除x.c的分配也没关系。标准允许这样的填充,但我想我可以安全地猜测你正在使用的实际实现没有它; - )

解释是x在抽象机器中具有已定义的状态,其值为abcx还有一个“对象表示”,它是内存中sizeof(stX)个字节的序列。您对memcpy的调用会从2*sizeof(int)的地址开始访问这些字节的x.b。所有这些都是合法的,并且保证x.b从结构的末尾开始至少有2*sizeof(int)个字节,因为C保证结构的数据成员按照它们的顺序排列定义。因此c必须在b之后。实现的唯一自由是它是否紧接着,或者它们之间是否存在填充。

允许编译器“知道”标准中memcpy的定义方式,因此可以推断出对memcpy的此调用不会访问x.a的任何字节,因为他们在x.b之前。这允许它进行优化,使得物理内存中的那些字节不包含它们在抽象机器中具有的值。但是,不允许推断对memcpy的调用不访问x.c的任何字节(除非结构中有太多填充),因此不允许省略复制这些字节的正确值到buffer

如果在x.b末尾读取的附加字节是填充而不是x.c的内容,那么当然,对x.c的分配是不相关的,因此可能是删去。

另一个合法的优化是将代码转换为:

int main() {
    ((int*)buffer)[0] = 2;
    ((int*)buffer)[1] = 3;
    ... same printf loop as before ...
}

也就是说,x(或者就此而言buffer)并不要求在物理内存中的任何地方“真正存在”,但标准确实需要打印出来的字节根据实施中int的表示方式以及实施中struct stX的布局方式,应该是正确的字节数。