6

考虑以下代码:

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;
}

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

当然,将结构实例定义为 volatile 会导致包含的分配。

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

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

4

4 回答 4

3

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

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

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


* 有关更严格的定义,请参阅 C99 标准的第 5.1.2.3 节:

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

...

如果实际实现可以推断出它的值没有被使用并且没有产生所需的副作用,则它不需要评估表达式的一部分

于 2013-01-25T16:00:17.730 回答
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不执行。它可能会进一步优化。

于 2013-01-25T16:06:35.597 回答
0

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

来自维基百科

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)
        ;
}
于 2013-01-25T16:10:52.523 回答
0

几个初步的观点:

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

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

我会假装您编写了一个程序,该程序从以下位置打印2*sizeof(int)字节:buffermemcpy

#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)有填充字节,那么删除分配也可以。该标准允许这样的填充,但我想我可以有把握地猜测你正在使用的实际实现没有它;-)bcstruct stXx.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在您的实施中进行布局。

于 2013-01-26T19:43:32.330 回答