请考虑以下代码:
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。另外,假设在函数调用之后使用缓冲区。我相应地更新了上面的示例代码。
答案 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)
是。编译器可以对代码应用任何优化,假设变量值不能“自行”更改。
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
,这意味着就标准而言,您的程序行为未定义。但我们假设您的实现明确允许void
从main
返回。此外,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)
中的b
和c
之间至少有struct stX
个字节的填充,则删除x.c
的分配也没关系。标准允许这样的填充,但我想我可以安全地猜测你正在使用的实际实现没有它; - )
解释是x
在抽象机器中具有已定义的状态,其值为a
,b
和c
。 x
还有一个“对象表示”,它是内存中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
的布局方式,应该是正确的字节数。