我在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
是的,你可以看到一个函数被转换为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
的传播在内联后被破坏了,由于一个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