[英]Why does gcc generate a memmove instead of a memcpy for copying a std::vector<>?
[英]Why does my C code call memmove (instead of memcpy)
我在 Linux 上使用 gcc 12.2。 我使用-nostdlib
,编译器抱怨缺少 memcpy 和 memmove。 所以我在汇编中实现了一个错误的 memcpy,并且我有 memmove 调用中止,因为我一直想使用 memcpy。
我想知道如果我在 C 中实现自己的函数,是否可以避免编译器要求 memcpy(和 memmove)。优化器似乎注意到它的真实含义并调用 C 函数。 然而,自从它被实施(我使用#define memcpy mymemcpy
)并且因为我运行它,我看到我的应用程序中止。 它调用了我的 memmove 实现而不是程序集 memcpy。 为什么 gcc 调用移动而不是复制?
clang 调用 memcpy 但 gcc 更好地优化了我的代码,所以我用它来优化构建
__attribute__ ((access(write_only, 1))) __attribute__((nonnull(1, 2)))
inline void mymemcpy(void *__restrict__ dest, const void *__restrict__ src, int size)
{
const unsigned char *s = (const unsigned char*)src;
unsigned char *d = (unsigned char*)dest;
while(size--) *d++ = *s++;
}
可重现
//dummy.cpp
extern "C" {
void*malloc() { return 0; }
int read() { return 0; }
int write() { return 0; }
int memcpy() { return 0; }
int memmove() { return 0; }
}
//main.cpp
#include <unistd.h>
#include <cstdlib>
struct MyVector {
void*p;
long long position, length;
};
__attribute__ ((access(write_only, 1))) __attribute__((nonnull(1, 2)))
void mymemcpy(void *__restrict__ dest, const void *__restrict__ src, int size)
{
const unsigned char *s = (const unsigned char*)src;
unsigned char *d = (unsigned char*)dest;
while(size--) *d++ = *s++;
}
//__attribute__ ((noinline))
int func(const char*file_from_disk, MyVector*v)
{
if (v->position + 5 <= v->length ) {
mymemcpy(v->p, file_from_disk, 5);
}
return 0;
}
char buf[4096];
extern "C"
int _start() {
MyVector v{malloc(1024),0,1024};
v.position += read(0, v.p, 1024-5);
int len = read(0, buf, 4096);
func(buf, &v);
write(1, v.p, v.position);
}
g++ -march=native -nostdlib -static -fno-exceptions -fno-rtti -O2 main.cpp 虚拟.cpp
使用objdump -D a.out | grep call
检查objdump -D a.out | grep call
401040: e8 db 00 00 00 call 401120 <memmove>
40108d: e8 4e 00 00 00 call 4010e0 <malloc>
4010a3: e8 48 00 00 00 call 4010f0 <read>
4010ba: e8 31 00 00 00 call 4010f0 <read>
4010c5: e8 56 ff ff ff call 401020 <_Z4funcPKcP8MyVector>
4010d5: e8 26 00 00 00 call 401100 <write>
402023: ff 11 call *(%rcx)
一个确切的答案需要深入研究 GCC 执行的代码转换并查看 GCC 如何转换您的代码。 这超出了我在合理时间内所能做的,但我可以用更一般的术语向您展示发生了什么,而无需深入研究 GCC 内部结构。
这是疯狂的部分:如果您删除inline
,您将获得memcpy
。 使用inline
,你得到memmove
。 我将在 Godbolt 上展示结果,然后讨论编译器如何工作来解释它。
这是我放在Godbolt上的一些测试代码。
__attribute__ ((access(write_only, 1))) __attribute__((nonnull(1, 2)))
extern inline void mymemcpy(void *__restrict__ dest, const void *__restrict__ src, int size)
{
const unsigned char *s = (const unsigned char*)src;
unsigned char *d = (unsigned char*)dest;
while(size--) *d++ = *s++;
}
void test(void *dest, const void *src, int size)
{
mymemcpy(dest, src, size);
}
这是生成的程序集
mymemcpy:
test edx, edx
je .L1
mov edx, edx
jmp memcpy
.L1:
ret
test:
test edx, edx
je .L4
mov edx, edx
jmp memmove
.L4:
ret
是的,您可以看到一个函数正在转换为memcpy
或memmove
。 它不仅仅是相同的代码,它只是一个函数,它根据是否内联而进行不同的转换。 为什么?
您可能会认为 C 编译器会执行如下操作:
预处理+标记化源文件,
解析创建 AST,
类型检查,
优化,
发出代码。
实际上,“优化”项是通过代码的许多不同遍历,并且这些遍中的每一个都以不同的方式修改代码。 这些遍在编译期间的不同时间发生,并且一些优化遍可能发生多次。
特定优化过程发生的顺序会影响结果。 如果您先执行优化 X,然后优化 Y,您会得到与先执行 Y,然后再执行 X 不同的结果。可能一个转换将信息从程序的一部分传播到另一部分,然后另一个不同的转换作用于该信息。
为什么这在这里相关?
你可以在这里看到有一个restrict
指针src
和dest
。 由于这些指针是restrict
,GCC“应该”能够知道memcpy
是可以接受的,而memmove
不是必需的。
但是,这意味着必须将src
和dest
是restrict
指针的信息传播到最终转换为memmove
或memcpy
的循环,并且必须在转换发生之前传播该信息。 您可以先轻松地将循环转换为memmove
,然后再确定参数是restrict
,但为时已晚!
看起来,不知何故,当函数被内联时, src
和dest
被restrict
的信息正在丢失。 这为我们提供了几种不同的理论来解释为什么会发生这种情况:
可能由于错误,内联后restrict
的传播以某种方式被破坏。
假设调用函数比被内联的函数具有更多的上下文,GCC 可能会在内联后从调用函数中推断出restrict
。
也许优化传递没有以正确的顺序发生, restrict
传播到循环。 也许该信息传播,然后内联执行,然后循环优化发生在这之后。
毕竟,优化通道(代码转换通道)对重新排序很敏感。 这是编译器设计的一个极其复杂的领域。
使用-fno-tree-loop-distribute-patterns
,或使用 pragma:
#pragma GCC optimize ("no-tree-loop-distribute-patterns")
简单使用-fno-builtin
命令行选项。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.