为什么我的C代码调用memmove(而不是memcpy)



我在linux上使用gcc 12.2。我使用-nostdlib,编译器抱怨缺少memcpy和memmove。所以我在汇编中实现了一个坏的memcpy,我有memmove调用中止,因为我总是想使用memcpy。

我想知道如果我在C中实现自己的memcpy(和memmove),是否可以避免编译器要求memcpy(和memmove)。优化器似乎注意到它到底是什么,并调用C函数。然而,自从它被实现(我使用#define memcpy mymemcpy),因为我运行它,我看到我的应用程序中止。它调用了我的memmove实现而不是汇编内存。为什么gcc调用move而不是copy?

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 - nodlib -static -fno-exceptions -fno-rtti -O2 main.cpp dummy.cpp

使用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

是的,你可以看到一个函数被转换为memcpymemmove。它不仅仅是相同的代码,它只是一个函数,根据它是否内联而进行不同的转换。为什么?

如何优化传递

你可能会认为C编译器是这样做的:

  1. 预处理+标记源文件,

  2. 解析创建AST,

  3. 类型检查,

  4. 优化,

  5. 发出代码。

实际上,这种"优化"项目是通过代码的许多不同的通道,每个通道以不同的方式修改代码。这些传递在编译过程中的不同时间发生,一些优化传递可能会发生多次。

特定优化传递的顺序会影响结果。如果您先执行优化X,然后再优化Y,那么您得到的结果与先执行Y,然后再执行X不同。也许一个转换将信息从程序的一部分传播到另一部分,然后另一个转换作用于该信息。

为什么这与这里相关?

你可以看到这里有一个restrict指针srcdest。由于这些指针是restrict, GCC应该"能够知道memcpy是可以接受的,而memmove是不必要的。

然而,这意味着srcdestrestrict指针的信息必须传播循环,最终转化为memmovememcpy该信息必须在转换发生之前传播。您可以轻松地首先将循环转换为memmove,然后再找出参数是restrict,但为时已晚!

看起来,不知何故,srcdestrestrict的信息在函数内联时丢失了。这给了我们两个不同的理论来解释为什么会发生这种情况:

  • 可能restrict的传播在内联后被破坏了,由于一个bug。

  • 可能GCC在内联之后从调用函数推断出restrict,假设调用函数比被内联的函数有更多的上下文。

  • 也许优化传递在这里没有以正确的顺序发生,以便restrict传播到循环。也许这个信息传播了,然后内联被执行,然后循环优化在那之后发生。

  • 毕竟,

优化传递(代码转换传递)对重新排序很敏感。这是编译器设计中一个极其复杂的领域。

禁用优化

使用-fno-tree-loop-distribute-patterns,或者使用pragma:

#pragma GCC optimize ("no-tree-loop-distribute-patterns")

简单使用-fno-builtin命令行选项

https://godbolt.org/z/3Ys1s9jPr

相关内容

  • 没有找到相关文章

最新更新